Skip to main content

excel_ooxml/
reader.rs

1//! Reading a `.xlsx` package into our minimal [`Workbook`] model.
2
3use std::collections::HashMap;
4use std::io::{Read, Seek};
5
6use opc::Package;
7use xml_core::{BytesStart, Event, Reader};
8
9use crate::error::{Error, Result};
10use crate::model::{
11    AutoFilterColumn, Border, BorderEdge, BorderStyle, CalculationMode, CalculationProperties,
12    Cell, CellAnchorPoint, CellComment, CellFormat, CellHyperlink, CellValue, ColumnSetting,
13    ComparisonOperator, ConditionalFormattingRule, CustomPropertyValue, DataValidationKind,
14    DataValidationRule, DefinedName, DocumentProperties, DrawingAnchor, DrawingObject, ExcelTable,
15    ExternalWorkbookLink, FreezePane, GradientFill, GradientStop, HorizontalAlignment,
16    HyperlinkTarget, IgnoredError, Message, NamedCellStyle, PageOrientation, PatternFill,
17    PatternType, PictureFormat, PrintSettings, ProtectedRange, RichTextRun, Row, Scenario, Sheet,
18    SheetChart, SheetDrawing, SheetPicture, SheetProtection, SheetVisibility, SortCondition,
19    SortState, TableColumn, TableStyle, TableStyleElement, TableStyleElementType,
20    TextComparisonOperator, TotalsRowFunction, VerticalAlignment, Workbook, WorkbookProtection,
21    WriteProtection,
22};
23
24/// Excel's own hard row/column limits (`.xlsx`'s `SpreadsheetML` schema caps a worksheet at
25/// 1,048,576 rows by 16,384 columns — one past `XFD`). A `<row r="..">`/cell reference/`<col
26/// min=".." max="..">` value from the file itself is never trusted past these bounds when it's
27/// about to size a `Vec` or drive a range loop: a malformed or adversarial `.xlsx` a few hundred
28/// bytes long could otherwise claim a row/column number in the billions and force an allocation of
29/// that size — a robustness audit found this to be a real, trivially reachable memory-exhaustion
30/// DoS against `Workbook::read_from`, not just a theoretical one.
31const EXCEL_MAX_ROWS: usize = 1_048_576;
32const EXCEL_MAX_COLUMNS: usize = 16_384;
33
34/// The relationship type identifying `xl/workbook.xml`, at the package root — mirrors
35/// `word-ooxml`'s `MAIN_DOCUMENT_RELATIONSHIP_TYPE`.
36const MAIN_WORKBOOK_RELATIONSHIP_TYPE: &str =
37    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
38
39/// The relationship type identifying `xl/sharedStrings.xml`, scoped to `xl/_rels/workbook.xml.rels`
40/// (like the worksheet relationships themselves) — located purely by relationship type, since
41/// nothing in `xl/workbook.xml`'s own content carries an explicit `r:id` for it.
42const SHARED_STRINGS_RELATIONSHIP_TYPE: &str =
43    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
44
45/// The relationship type identifying `xl/styles.xml`, same posture as
46/// `SHARED_STRINGS_RELATIONSHIP_TYPE`.
47const STYLES_RELATIONSHIP_TYPE: &str =
48    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
49
50/// The relationship types identifying a worksheet's own `xl/tables/tableN.xml` and
51/// `xl/commentsN.xml`, scoped to that worksheet's own `.rels`. Mirrors `writer.rs`'s own (private,
52/// so duplicated rather than shared) constants of the same name.
53const TABLE_RELATIONSHIP_TYPE: &str =
54    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table";
55const COMMENTS_RELATIONSHIP_TYPE: &str =
56    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
57
58// a sheet's drawing (embedded pictures/charts).
59const DRAWING_RELATIONSHIP_TYPE: &str =
60    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
61const IMAGE_RELATIONSHIP_TYPE: &str =
62    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
63const CHART_RELATIONSHIP_TYPE: &str =
64    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
65
66// — mirrors `writer.rs`'s own (private) constants of the same name.
67const CORE_PROPERTIES_RELATIONSHIP_TYPE: &str =
68    "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
69const EXTENDED_PROPERTIES_RELATIONSHIP_TYPE: &str =
70    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
71const VBA_PROJECT_RELATIONSHIP_TYPE: &str =
72    "http://schemas.microsoft.com/office/2006/relationships/vbaProject";
73// `docProps/custom.xml` — point 56, mirrors `writer.rs`'s own (private) constant of the same name.
74const CUSTOM_PROPERTIES_RELATIONSHIP_TYPE: &str =
75    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties";
76// Note: unlike `CORE_PROPERTIES_RELATIONSHIP_TYPE`/`EXTENDED_PROPERTIES_RELATIONSHIP_TYPE`/
77// `VBA_PROJECT_RELATIONSHIP_TYPE` above, an external link part is never looked up by relationship
78// *type* on the reading side — `<externalReference r:id="..">` already gives the exact relationship
79// id directly (see `parsed_workbook.external_reference_relationship_ids` below), so there is no
80// `EXTERNAL_LINK_RELATIONSHIP_TYPE` constant here (unlike `writer.rs`, which does need it to author
81// that relationship in the first place).
82const EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE: &str =
83    "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLinkPath";
84
85impl Workbook {
86    /// Reads a `.xlsx` package from a seekable byte source.
87    pub fn read_from<R: Read + Seek>(reader: R) -> Result<Self> {
88        let package = Package::read_from(reader)?;
89
90        let main_workbook_relationship = package
91            .relationships()
92            .iter()
93            .find(|relationship| relationship.rel_type == MAIN_WORKBOOK_RELATIONSHIP_TYPE)
94            .ok_or(Error::MissingWorkbook)?;
95
96        let workbook_part_name = format!("/{}", main_workbook_relationship.target);
97        let workbook_part = package
98            .part(&workbook_part_name)
99            .ok_or(Error::MissingWorkbook)?;
100        let workbook_xml = std::str::from_utf8(&workbook_part.data)
101            .map_err(|error| Error::InvalidUtf8(workbook_part_name.clone(), error))?;
102
103        let parsed_workbook = parse_workbook_xml(workbook_xml)?;
104        let sheet_entries = parsed_workbook.sheets;
105        let defined_names = parsed_workbook.defined_names;
106
107        // `docProps/core.xml`/`docProps/app.xml`. Resolved off the *package root* relationships
108        // (`_rels/.rels`), same as `xl/workbook.xml` itself, not `xl/_rels/workbook.xml.rels` —
109        // both are package-level parts. Tolerant of either part being missing (an arbitrary real
110        // `.xlsx` might lack one), in which case every field simply resolves to `None`.
111        let core_properties_relationship = package
112            .relationships()
113            .iter()
114            .find(|relationship| relationship.rel_type == CORE_PROPERTIES_RELATIONSHIP_TYPE);
115        let ParsedCoreProperties {
116            title,
117            author,
118            subject,
119            keywords,
120        } = match core_properties_relationship {
121            Some(relationship) => {
122                let part_name = format!("/{}", relationship.target);
123                match package.part(&part_name) {
124                    Some(part) => {
125                        let xml = std::str::from_utf8(&part.data)
126                            .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
127                        parse_core_properties_xml(xml)?
128                    }
129                    None => ParsedCoreProperties::default(),
130                }
131            }
132            None => ParsedCoreProperties::default(),
133        };
134        let extended_properties_relationship = package
135            .relationships()
136            .iter()
137            .find(|relationship| relationship.rel_type == EXTENDED_PROPERTIES_RELATIONSHIP_TYPE);
138        let (company, manager, hyperlink_base) = match extended_properties_relationship {
139            Some(relationship) => {
140                let part_name = format!("/{}", relationship.target);
141                match package.part(&part_name) {
142                    Some(part) => {
143                        let xml = std::str::from_utf8(&part.data)
144                            .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
145                        parse_extended_properties_xml(xml)?
146                    }
147                    None => (None, None, None),
148                }
149            }
150            None => (None, None, None),
151        };
152        // `docProps/custom.xml`. Same package-root-relationship resolution as `core.xml`/`app.xml`
153        // above, tolerant of the part/relationship being absent (resolves to an empty `Vec`, this
154        // crate's own default for `custom_properties` too).
155        let custom_properties_relationship = package
156            .relationships()
157            .iter()
158            .find(|relationship| relationship.rel_type == CUSTOM_PROPERTIES_RELATIONSHIP_TYPE);
159        let custom_properties = match custom_properties_relationship {
160            Some(relationship) => {
161                let part_name = format!("/{}", relationship.target);
162                match package.part(&part_name) {
163                    Some(part) => {
164                        let xml = std::str::from_utf8(&part.data)
165                            .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
166                        parse_custom_properties_xml(xml)?
167                    }
168                    None => Vec::new(),
169                }
170            }
171            None => Vec::new(),
172        };
173        let properties = DocumentProperties {
174            title,
175            author,
176            subject,
177            keywords,
178            company,
179            manager,
180            hyperlink_base,
181            custom_properties,
182        };
183
184        // `xl/vbaProject.bin`. Preserved as opaque raw bytes, see `Workbook.vba_project`'s doc
185        // comment.
186        let vba_project = workbook_part
187            .relationships
188            .iter()
189            .find(|relationship| relationship.rel_type == VBA_PROJECT_RELATIONSHIP_TYPE)
190            .and_then(|relationship| package.part(&format!("/xl/{}", relationship.target)))
191            .map(|part| part.data.clone());
192
193        // `xl/externalLinks/externalLinkN.xml`. Each entry's real external target path is itself
194        // resolved from *that part's own* `.rels` (`EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE`,
195        // `TargetMode="External"`), not `xl/_rels/workbook.xml.rels`.
196        let mut external_links = Vec::new();
197        for relationship_id in &parsed_workbook.external_reference_relationship_ids {
198            let Some(relationship) = workbook_part.relationships.by_id(relationship_id) else {
199                continue;
200            };
201            let part_name = format!("/xl/{}", relationship.target);
202            let Some(part) = package.part(&part_name) else {
203                continue;
204            };
205            let xml = std::str::from_utf8(&part.data)
206                .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
207            let ParsedExternalLink {
208                sheet_names,
209                defined_names,
210            } = parse_external_link_xml(xml)?;
211            let target = part
212                .relationships
213                .iter()
214                .find(|relationship| {
215                    relationship.rel_type == EXTERNAL_LINK_TARGET_RELATIONSHIP_TYPE
216                })
217                .map(|relationship| relationship.target.clone())
218                .unwrap_or_default();
219            external_links.push(ExternalWorkbookLink {
220                target,
221                sheet_names,
222                defined_names,
223            });
224        }
225
226        // `xl/sharedStrings.xml` is resolved via `xl/workbook.xml`'s own relationships
227        // (`xl/_rels/workbook.xml.rels`) purely by relationship type, same posture as
228        // `word-ooxml`'s `STYLES_RELATIONSHIP_TYPE` — tolerant of the part being missing entirely
229        // (a workbook with no text cells at all has no reason to carry one), in which case every
230        // text cell simply resolves to no string (see `parse_worksheet_xml`).
231        let shared_strings_relationship = workbook_part
232            .relationships
233            .iter()
234            .find(|relationship| relationship.rel_type == SHARED_STRINGS_RELATIONSHIP_TYPE);
235        let shared_strings = match shared_strings_relationship {
236            Some(relationship) => {
237                let part_name = format!("/xl/{}", relationship.target);
238                match package.part(&part_name) {
239                    Some(part) => {
240                        let xml = std::str::from_utf8(&part.data)
241                            .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
242                        parse_shared_strings_xml(xml)?
243                    }
244                    None => Vec::new(),
245                }
246            }
247            None => Vec::new(),
248        };
249
250        // `xl/styles.xml` is resolved the same way — tolerant of the part being missing entirely
251        // (though this crate's own writer always writes one, an arbitrary real `.xlsx` might not if
252        // it truly has zero cell formatting, however unlikely). `cell_formats[i]` is
253        // `xl/styles.xml`'s `cellXfs` entry at index `i`, resolved down to an `Option<CellFormat>`
254        // — index 0 (the mandatory "Normal" style) always resolves to `None`, see
255        // `resolve_cell_format`. `dxfs` is `xl/styles.xml`'s `<dxfs>` collection, resolved down to
256        // plain `CellFormat`s — a separate index space from `cell_formats` (see
257        // `parse_styles_xml`'s doc comment), used to resolve a conditional formatting rule's
258        // `dxfId`.
259        let styles_relationship = workbook_part
260            .relationships
261            .iter()
262            .find(|relationship| relationship.rel_type == STYLES_RELATIONSHIP_TYPE);
263        let parsed_styles: ParsedStyles = match styles_relationship {
264            Some(relationship) => {
265                let part_name = format!("/xl/{}", relationship.target);
266                match package.part(&part_name) {
267                    Some(part) => {
268                        let xml = std::str::from_utf8(&part.data)
269                            .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
270                        parse_styles_xml(xml)?
271                    }
272                    None => ParsedStyles::default(),
273                }
274            }
275            None => ParsedStyles::default(),
276        };
277        let ParsedStyles {
278            cell_formats,
279            dxfs,
280            table_styles,
281            named_cell_styles,
282        } = parsed_styles;
283
284        let mut sheets = Vec::with_capacity(sheet_entries.len());
285        for (name, relationship_id, visibility) in sheet_entries {
286            let relationship = workbook_part
287                .relationships
288                .by_id(&relationship_id)
289                .ok_or_else(|| Error::MissingSheetRelationship(relationship_id.clone()))?;
290            let worksheet_part_name = format!("/xl/{}", relationship.target);
291            let worksheet_part = package
292                .part(&worksheet_part_name)
293                .ok_or_else(|| Error::MissingWorksheetPart(worksheet_part_name.clone()))?;
294            let worksheet_xml = std::str::from_utf8(&worksheet_part.data)
295                .map_err(|error| Error::InvalidUtf8(worksheet_part_name.clone(), error))?;
296            let sheet_relationships = &worksheet_part.relationships;
297            let parsed = parse_worksheet_xml(
298                worksheet_xml,
299                &shared_strings,
300                &cell_formats,
301                &dxfs,
302                sheet_relationships,
303            )?;
304
305            // `xl/commentsN.xml`/`xl/tables/tableN.xml` are resolved the same way
306            // `xl/styles.xml`/`xl/sharedStrings.xml` are off the workbook part — purely by
307            // relationship type, scoped to this worksheet's own `.rels` this time. Both tolerant of
308            // the relationship or the part itself being missing (an unprotected/table-less sheet
309            // has no reason to carry either).
310            let comments = match sheet_relationships
311                .iter()
312                .find(|relationship| relationship.rel_type == COMMENTS_RELATIONSHIP_TYPE)
313            {
314                Some(relationship) => {
315                    let part_name =
316                        format!("/xl/{}", relationship.target.trim_start_matches("../"));
317                    match package.part(&part_name) {
318                        Some(part) => {
319                            let xml = std::str::from_utf8(&part.data)
320                                .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
321                            parse_comments_xml(xml)?
322                        }
323                        None => Vec::new(),
324                    }
325                }
326                None => Vec::new(),
327            };
328
329            let table = match sheet_relationships
330                .iter()
331                .find(|relationship| relationship.rel_type == TABLE_RELATIONSHIP_TYPE)
332            {
333                Some(relationship) => {
334                    let part_name =
335                        format!("/xl/{}", relationship.target.trim_start_matches("../"));
336                    match package.part(&part_name) {
337                        Some(part) => {
338                            let xml = std::str::from_utf8(&part.data)
339                                .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
340                            Some(parse_table_xml(xml)?)
341                        }
342                        None => None,
343                    }
344                }
345                None => None,
346            };
347
348            // `xl/drawings/drawingN.xml`. Same resolve-by-relationship-type posture as
349            // `comments`/`table` above, tolerant of the relationship or the part itself being
350            // missing.
351            let drawing = match sheet_relationships
352                .iter()
353                .find(|relationship| relationship.rel_type == DRAWING_RELATIONSHIP_TYPE)
354            {
355                Some(relationship) => {
356                    let part_name =
357                        format!("/xl/{}", relationship.target.trim_start_matches("../"));
358                    match package.part(&part_name) {
359                        Some(part) => {
360                            let xml = std::str::from_utf8(&part.data)
361                                .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
362                            Some(parse_drawing_xml(xml, &part.relationships, &package)?)
363                        }
364                        None => None,
365                    }
366                }
367                None => None,
368            };
369
370            sheets.push(Sheet {
371                name,
372                rows: parsed.rows,
373                conditional_formatting_rules: parsed.conditional_formatting_rules,
374                data_validation_rules: parsed.data_validation_rules,
375                merged_ranges: parsed.merged_ranges,
376                column_settings: parsed.column_settings,
377                auto_filter_range: parsed.auto_filter_range,
378                freeze_panes: parsed.freeze_panes,
379                print_settings: parsed.print_settings,
380                protection: parsed.protection,
381                hyperlinks: parsed.hyperlinks,
382                comments,
383                table,
384                drawing,
385                visibility,
386                zoom_scale: parsed.zoom_scale,
387                row_breaks: parsed.row_breaks,
388                column_breaks: parsed.column_breaks,
389                scenarios: parsed.scenarios,
390                auto_filter_columns: parsed.auto_filter_columns,
391                tab_color: parsed.tab_color,
392                show_grid_lines: parsed.show_grid_lines,
393                show_row_col_headers: parsed.show_row_col_headers,
394                show_zeros: parsed.show_zeros,
395                right_to_left: parsed.right_to_left,
396                active_cell: parsed.active_cell,
397                default_column_width: parsed.default_column_width,
398                default_row_height: parsed.default_row_height,
399                ignored_errors: parsed.ignored_errors,
400                protected_ranges: parsed.protected_ranges,
401                sort_state: parsed.sort_state,
402                outline_summary_below: parsed.outline_summary_below,
403                outline_summary_right: parsed.outline_summary_right,
404            });
405        }
406
407        Ok(Workbook {
408            sheets,
409            defined_names,
410            properties,
411            protection: parsed_workbook.protection,
412            write_protection: parsed_workbook.write_protection,
413            active_tab: parsed_workbook.active_tab,
414            external_links,
415            vba_project,
416            calculation: parsed_workbook.calculation,
417            table_styles,
418            named_cell_styles,
419        })
420    }
421}
422
423/// Parses `xl/workbook.xml`'s `<sheets>` list into `(name, relationship_id)` pairs, in document
424/// order — the inverse of `to_workbook_xml`'s `<sheet name=".." sheetId=".." r:id="..">`. `sheetId`
425/// is not read back (this crate never needs it: sheets are always addressed by their position in
426/// `Workbook.sheets`, not by `ST_SheetId`) — and, in the function's second return value,
427/// `xl/workbook.xml`'s `<definedNames>` list into [`DefinedName`]s.
428///
429/// A `<definedName>`'s reference/formula text is the element's own text content, not a dedicated
430/// child (unlike a conditional formatting rule's `<formula>`) — its accumulator handles
431/// `Event::Text` **and** `Event::GeneralRef` from the start (see the `GeneralRef`-handling bug
432/// fixed for the previous round: `quick_xml` 0.41 splits any `&entity;` reference in text out into
433/// its own event). The result of parsing `xl/workbook.xml` — adds
434/// `protection`/`active_tab`/`external_reference_relationship_ids` and a `hidden` flag per sheet
435/// entry alongside the original `(name, relationship_id)`/`defined_names`; upgrades the per-sheet
436/// flag to a full [`SheetVisibility`] and adds `calculation`.
437struct ParsedWorkbook {
438    /// `(name, r:id, visibility)` per `<sheet>`, in document order.
439    sheets: Vec<(String, String, SheetVisibility)>,
440    defined_names: Vec<DefinedName>,
441    /// `<fileSharing>`, if present. Its password (if any) is never recoverable, same one-way
442    /// posture as `protection` below.
443    write_protection: Option<WriteProtection>,
444    /// `<workbookProtection>`, if present — the password (if any) is never recoverable, same
445    /// one-way posture as `SheetProtection`, so this always resolves to `password: None` when
446    /// present at all.
447    protection: Option<WorkbookProtection>,
448    /// `<bookViews><workbookView activeTab="..">`, if present.
449    active_tab: Option<usize>,
450    /// `<externalReferences><externalReference r:id="..">`'s ids, in order — resolved into
451    /// [`ExternalWorkbookLink`]s by `read_from`.
452    external_reference_relationship_ids: Vec<String>,
453    /// `<calcPr>`, if present.
454    calculation: Option<CalculationProperties>,
455}
456
457fn parse_workbook_xml(xml: &str) -> Result<ParsedWorkbook> {
458    let mut reader = Reader::from_xml_str(xml);
459    let mut sheets = Vec::new();
460    let mut defined_names = Vec::new();
461    let mut write_protection: Option<WriteProtection> = None;
462    let mut protection: Option<WorkbookProtection> = None;
463    let mut active_tab: Option<usize> = None;
464    let mut external_reference_relationship_ids = Vec::new();
465    let mut calculation: Option<CalculationProperties> = None;
466
467    let mut in_defined_name = false;
468    let mut current_name: Option<String> = None;
469    let mut current_local_sheet_id: Option<usize> = None;
470    let mut current_hidden = false;
471    let mut current_comment: Option<String> = None;
472    let mut refers_to_buffer = String::new();
473
474    loop {
475        match reader.read_event()? {
476            Event::Eof => break,
477
478            Event::Start(start) | Event::Empty(start)
479                if start.local_name().as_ref() == b"sheet" =>
480            {
481                let name = raw_attribute(&start, b"name").unwrap_or_default();
482                let relationship_id = raw_attribute(&start, b"id").unwrap_or_default();
483                let visibility = match raw_attribute(&start, b"state").as_deref() {
484                    Some("hidden") => SheetVisibility::Hidden,
485                    Some("veryHidden") => SheetVisibility::VeryHidden,
486                    _ => SheetVisibility::Visible,
487                };
488                sheets.push((name, relationship_id, visibility));
489            }
490
491            Event::Empty(start) if start.local_name().as_ref() == b"calcPr" => {
492                let mode = match raw_attribute(&start, b"calcMode").as_deref() {
493                    Some("manual") => Some(CalculationMode::Manual),
494                    Some("autoNoTable") => Some(CalculationMode::AutomaticExceptTables),
495                    Some("auto") => Some(CalculationMode::Automatic),
496                    _ => None,
497                };
498                let iterate = raw_attribute(&start, b"iterate").as_deref() == Some("1");
499                calculation = Some(CalculationProperties { mode, iterate });
500            }
501
502            Event::Empty(start) if start.local_name().as_ref() == b"workbookProtection" => {
503                protection = Some(WorkbookProtection {
504                    password: None,
505                    lock_structure: raw_attribute(&start, b"lockStructure").as_deref() == Some("1"),
506                    lock_windows: raw_attribute(&start, b"lockWindows").as_deref() == Some("1"),
507                });
508            }
509
510            // `<fileSharing>`.
511            Event::Empty(start) if start.local_name().as_ref() == b"fileSharing" => {
512                write_protection = Some(WriteProtection {
513                    read_only_recommended: raw_attribute(&start, b"readOnlyRecommended").as_deref()
514                        == Some("1"),
515                    user_name: raw_attribute(&start, b"userName"),
516                    password: None,
517                });
518            }
519
520            Event::Empty(start) if start.local_name().as_ref() == b"workbookView" => {
521                active_tab = raw_attribute(&start, b"activeTab")
522                    .and_then(|value| value.parse::<usize>().ok());
523            }
524
525            Event::Empty(start) if start.local_name().as_ref() == b"externalReference" => {
526                if let Some(id) = raw_attribute(&start, b"id") {
527                    external_reference_relationship_ids.push(id);
528                }
529            }
530
531            Event::Start(start) if start.local_name().as_ref() == b"definedName" => {
532                in_defined_name = true;
533                current_name = raw_attribute(&start, b"name");
534                current_local_sheet_id = raw_attribute(&start, b"localSheetId")
535                    .and_then(|value| value.parse::<usize>().ok());
536                current_hidden = raw_attribute(&start, b"hidden").as_deref() == Some("1");
537                current_comment = raw_attribute(&start, b"comment");
538                refers_to_buffer.clear();
539            }
540            Event::Empty(start) if start.local_name().as_ref() == b"definedName" => {
541                // A self-closing `<definedName../>` — schema-valid (an empty reference), not
542                // written by this crate's own writer but tolerated when reading an arbitrary real
543                // file.
544                if let Some(name) = raw_attribute(&start, b"name") {
545                    defined_names.push(DefinedName {
546                        name,
547                        refers_to: String::new(),
548                        local_sheet_id: raw_attribute(&start, b"localSheetId")
549                            .and_then(|value| value.parse::<usize>().ok()),
550                        hidden: raw_attribute(&start, b"hidden").as_deref() == Some("1"),
551                        comment: raw_attribute(&start, b"comment"),
552                    });
553                }
554            }
555            Event::End(end) if in_defined_name && end.local_name().as_ref() == b"definedName" => {
556                in_defined_name = false;
557                if let Some(name) = current_name.take() {
558                    defined_names.push(DefinedName {
559                        name,
560                        refers_to: std::mem::take(&mut refers_to_buffer),
561                        local_sheet_id: current_local_sheet_id.take(),
562                        hidden: std::mem::take(&mut current_hidden),
563                        comment: current_comment.take(),
564                    });
565                }
566            }
567            Event::Text(text) if in_defined_name => {
568                refers_to_buffer.push_str(&xml_core::decode_text(&text)?);
569            }
570            Event::GeneralRef(reference) if in_defined_name => {
571                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
572                    refers_to_buffer.push_str(&resolved);
573                }
574            }
575
576            _ => {}
577        }
578    }
579
580    Ok(ParsedWorkbook {
581        sheets,
582        defined_names,
583        write_protection,
584        protection,
585        active_tab,
586        external_reference_relationship_ids,
587        calculation,
588    })
589}
590
591/// One entry of a workbook's shared strings table, as parsed back from `xl/sharedStrings.xml` — the
592/// reader-side mirror of `writer.rs`'s `SharedStringEntry`, kept as a separate (structurally
593/// identical) type since the two modules don't share private items.
594#[derive(Debug, Clone, PartialEq)]
595enum SharedStringEntry {
596    Plain(String),
597    Rich(Vec<RichTextRun>),
598}
599
600/// Parses `xl/sharedStrings.xml` (`CT_Sst`) into a `Vec<SharedStringEntry>`, indexed exactly like
601/// the file itself (`<si>` at position `N` corresponds to a cell's `<v>N</v>` when `t="s"`). An
602/// `<si>` is parsed as `Plain` when it holds a single direct `<t>` (`CT_Rst`'s simple form), or
603/// `Rich` when it holds one or more `<r>` runs (`CT_Rst`'s rich-text form, used when a string has
604/// mixed character formatting). A run's `<rPr>` is decoded into the same bold/italic/size/color
605/// subset [`RichTextRun`] models on the write side (see `writer.rs::to_shared_strings_xml`'s doc
606/// comment for why no `rFont`/`family`).
607fn parse_shared_strings_xml(xml: &str) -> Result<Vec<SharedStringEntry>> {
608    let mut reader = Reader::from_xml_str(xml);
609    let mut entries = Vec::new();
610
611    let mut in_si = false;
612    let mut si_is_rich = false;
613    let mut si_plain_buffer = String::new();
614    let mut si_runs: Vec<RichTextRun> = Vec::new();
615
616    let mut in_run = false;
617    let mut run_text_buffer = String::new();
618    let mut run_bold = false;
619    let mut run_italic = false;
620    let mut run_size: Option<f64> = None;
621    let mut run_color: Option<String> = None;
622    let mut run_name: Option<String> = None;
623    let mut run_underline = false;
624    let mut run_strike = false;
625    let mut in_rpr = false;
626    let mut in_text = false;
627
628    loop {
629        match reader.read_event()? {
630            Event::Eof => break,
631
632            Event::Start(start) if start.local_name().as_ref() == b"si" => {
633                in_si = true;
634                si_is_rich = false;
635                si_plain_buffer.clear();
636                si_runs.clear();
637            }
638            Event::End(end) if end.local_name().as_ref() == b"si" => {
639                in_si = false;
640                if si_is_rich {
641                    entries.push(SharedStringEntry::Rich(std::mem::take(&mut si_runs)));
642                } else {
643                    entries.push(SharedStringEntry::Plain(std::mem::take(
644                        &mut si_plain_buffer,
645                    )));
646                }
647            }
648
649            Event::Start(start) if in_si && start.local_name().as_ref() == b"r" => {
650                in_run = true;
651                si_is_rich = true;
652                run_text_buffer.clear();
653                run_bold = false;
654                run_italic = false;
655                run_size = None;
656                run_color = None;
657                run_name = None;
658                run_underline = false;
659                run_strike = false;
660            }
661            Event::End(end) if in_si && end.local_name().as_ref() == b"r" => {
662                in_run = false;
663                si_runs.push(RichTextRun {
664                    text: std::mem::take(&mut run_text_buffer),
665                    bold: std::mem::take(&mut run_bold),
666                    italic: std::mem::take(&mut run_italic),
667                    font_color: run_color.take(),
668                    font_size: run_size.take(),
669                    font_name: run_name.take(),
670                    underline: std::mem::take(&mut run_underline),
671                    strike: std::mem::take(&mut run_strike),
672                });
673            }
674
675            Event::Start(start) if in_run && start.local_name().as_ref() == b"rPr" => {
676                in_rpr = true;
677            }
678            Event::End(end) if in_run && end.local_name().as_ref() == b"rPr" => {
679                in_rpr = false;
680            }
681            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"b" => {
682                run_bold = true;
683            }
684            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"i" => {
685                run_italic = true;
686            }
687            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"strike" => {
688                run_strike = true;
689            }
690            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"u" => {
691                run_underline = true;
692            }
693            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"rFont" => {
694                run_name = raw_attribute(&start, b"val");
695            }
696            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"sz" => {
697                run_size =
698                    raw_attribute(&start, b"val").and_then(|value| value.parse::<f64>().ok());
699            }
700            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"color" => {
701                run_color = resolve_color(&start);
702            }
703
704            Event::Start(start) if start.local_name().as_ref() == b"t" => {
705                in_text = true;
706            }
707            Event::End(end) if end.local_name().as_ref() == b"t" => {
708                in_text = false;
709            }
710
711            Event::Text(text) if in_text && in_run => {
712                run_text_buffer.push_str(&xml_core::decode_text(&text)?);
713            }
714            Event::GeneralRef(reference) if in_text && in_run => {
715                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
716                    run_text_buffer.push_str(&resolved);
717                }
718            }
719            Event::Text(text) if in_text && !in_run => {
720                si_plain_buffer.push_str(&xml_core::decode_text(&text)?);
721            }
722            Event::GeneralRef(reference) if in_text && !in_run => {
723                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
724                    si_plain_buffer.push_str(&resolved);
725                }
726            }
727
728            _ => {}
729        }
730    }
731
732    Ok(entries)
733}
734
735/// A single raw `<font>` entry, as literally declared in `xl/styles.xml`'s `<fonts>` — resolved
736/// into a [`CellFormat`]'s font fields by `resolve_cell_format`.
737struct RawFont {
738    bold: bool,
739    italic: bool,
740    size: f64,
741    color: Option<String>,
742    name: Option<String>,
743    underline: bool,
744    strike: bool,
745}
746
747/// A single raw `<fill>` entry, as literally declared in `xl/styles.xml`'s `<fills>`.
748/// `Pattern`/`Gradient` are.
749enum RawFill {
750    None,
751    Gray125,
752    Solid(String),
753    Pattern(PatternFill),
754    Gradient(GradientFill),
755    /// A recognized-but-otherwise-malformed fill (e.g. a `solid` pattern with no foreground color)
756    /// — resolves like no fill at all.
757    Other,
758}
759
760/// A single raw `<border>` entry, as literally declared in `xl/styles.xml`'s `<borders>`.
761#[derive(Default)]
762struct RawBorder {
763    top: Option<(String, Option<String>)>,
764    bottom: Option<(String, Option<String>)>,
765    left: Option<(String, Option<String>)>,
766    right: Option<(String, Option<String>)>,
767    diagonal: Option<(String, Option<String>)>,
768    diagonal_up: bool,
769    diagonal_down: bool,
770}
771
772/// A single raw `<xf>` entry, as literally declared in `xl/styles.xml`'s `<cellXfs>`.
773struct RawXf {
774    num_fmt_id: u32,
775    font_id: usize,
776    fill_id: usize,
777    border_id: usize,
778    /// This entry's own `xfId` (index into `cellStyleXfs`). `0` (the default) means "Normal"/no
779    /// named style link.
780    xf_id: usize,
781    horizontal: Option<String>,
782    vertical: Option<String>,
783    wrap_text: bool,
784    indent: Option<u32>,
785    text_rotation: Option<u16>,
786    shrink_to_fit: bool,
787    locked: Option<bool>,
788    formula_hidden: Option<bool>,
789}
790
791/// Parses `xl/styles.xml` (`CT_Stylesheet`) into a `Vec<Option<CellFormat>>` indexed exactly like
792/// the file's own `<cellXfs>` (position `N` corresponds to a cell's `<c s="N">`) — index 0 (Excel's
793/// mandatory "Normal" style baseline, always present) always resolves to `None`, see
794/// `resolve_cell_format`.
795///
796/// Only a `numFmtId` actually declared in this file's own `<numFmts>` is decoded back to a
797/// format-code string (this crate's own writer never reuses a built-in id, see
798/// `FIRST_CUSTOM_NUM_FMT_ID` in `writer.rs`, but an arbitrary real `.xlsx` might reference a
799/// built-in one — those read back as `None`). Only a `solid` fill's foreground color is decoded to
800/// `CellFormat::fill_color` — any other pattern type reads back as no fill.
801///
802/// Also parses `<dxfs>` into a second, separate `Vec<CellFormat>` (the function's second return
803/// value) — unlike `cellXfs`'s entries, a `<dxf>`'s font/numFmt/fill/alignment children are
804/// inline/self-contained rather than index-references into `fonts`/`fills`/`num_fmts` (see
805/// `CT_Dxf`'s doc comment in `writer.rs`), so each `<dxf>` is resolved directly into a `CellFormat`
806/// as it's parsed, with no separate `resolve_*` indirection needed. A rule's `dxfId` is a direct
807/// index into this second `Vec`.
808///
809/// Also parses `<tableStyles>`/`<tableStyle>`/`<tableStyleElement>` into a third `Vec<TableStyle>`
810/// (the function's third return value). Each element's `dxfId` resolves against the second
811/// `Vec<CellFormat>` (`<dxfs>`), which — since `CT_Stylesheet`'s own sequence puts `dxfs` before
812/// `tableStyles` — is always already fully parsed by the time a `<tableStyleElement>` is reached.
813/// The four collections [`parse_styles_xml`] parses out of `xl/styles.xml` — grouped into a struct
814/// instead of a 4-tuple (clippy's `type_complexity`; named fields also read better at every call
815/// site than `.0`/`.1`/`.2`/`.3` would).
816#[derive(Default)]
817struct ParsedStyles {
818    cell_formats: Vec<Option<CellFormat>>,
819    dxfs: Vec<CellFormat>,
820    table_styles: Vec<TableStyle>,
821    named_cell_styles: Vec<NamedCellStyle>,
822}
823
824fn parse_styles_xml(xml: &str) -> Result<ParsedStyles> {
825    let mut reader = Reader::from_xml_str(xml);
826
827    let mut num_fmts: HashMap<u32, String> = HashMap::new();
828    let mut fonts: Vec<RawFont> = Vec::new();
829    let mut fills: Vec<RawFill> = Vec::new();
830    let mut borders: Vec<RawBorder> = Vec::new();
831    let mut xfs: Vec<RawXf> = Vec::new();
832
833    let mut current_font: Option<RawFont> = None;
834    let mut in_fill = false;
835    let mut current_fill_pattern_type: Option<String> = None;
836    let mut current_fill_fg_color: Option<String> = None;
837    let mut current_fill_bg_color: Option<String> = None;
838    let mut current_fill_is_gradient = false;
839    let mut current_gradient_angle: Option<f64> = None;
840    let mut current_gradient_stops: Vec<GradientStop> = Vec::new();
841    let mut current_gradient_stop_position: Option<f64> = None;
842    // `<borders>` parsing state — kept outside `in_dxfs` scope entirely (a `<dxf>`'s own border
843    // child, if any, isn't modeled — see `CellFormat.border`'s doc comment: dxfs don't cover
844    // borders).
845    let mut in_borders = false;
846    let mut current_border: Option<RawBorder> = None;
847    let mut current_border_edge_name: Option<&'static str> = None;
848    let mut current_border_edge_style: Option<String> = None;
849    let mut current_border_edge_color: Option<String> = None;
850    let mut in_cell_xfs = false;
851    let mut current_xf: Option<RawXf> = None;
852
853    // `<cellStyleXfs>`/`<xf>` and `<cellStyles>`/`<cellStyle>`. `style_xfs` mirrors `xfs` above but
854    // for the `cellStyleXfs` collection (a separate index space, resolved into `CellFormat`s the
855    // same way, sharing the same `fonts`/`fills`/ `borders`/`num_fmts`); `cell_styles` is `(name,
856    // xf_id, builtin_id)` per `<cellStyle>`, in document order — `xf_id` is that element's own
857    // `xfId` attribute (an index into `style_xfs`), used both to build `named_cell_styles` and, via
858    // `style_name_by_xf_id`, to resolve a `cellXfs` entry's own `xfId` back into
859    // `CellFormat::named_style`.
860    let mut in_cell_style_xfs = false;
861    let mut current_style_xf: Option<RawXf> = None;
862    let mut style_xfs: Vec<RawXf> = Vec::new();
863    let mut cell_styles: Vec<(String, usize, Option<u32>)> = Vec::new();
864
865    // `<dxfs>` parsing state — kept entirely separate from the above (see this function's doc
866    // comment for why a dxf's children are inline rather than index-references).
867    let mut in_dxfs = false;
868    let mut current_dxf: Option<CellFormat> = None;
869    let mut in_dxf_font = false;
870    let mut dxf_font_bold = false;
871    let mut dxf_font_italic = false;
872    let mut dxf_font_color: Option<String> = None;
873    let mut in_dxf_fill = false;
874    let mut dxf_fill_pattern_type: Option<String> = None;
875    let mut dxf_fill_bg_color: Option<String> = None;
876    let mut dxfs: Vec<CellFormat> = Vec::new();
877
878    // `<tableStyles>`/`<tableStyle>`/`<tableStyleElement>`.
879    let mut table_styles: Vec<TableStyle> = Vec::new();
880    let mut current_table_style: Option<TableStyle> = None;
881
882    loop {
883        match reader.read_event()? {
884            Event::Eof => break,
885
886            Event::Start(start) if start.local_name().as_ref() == b"tableStyle" => {
887                current_table_style = Some(TableStyle::new(
888                    raw_attribute(&start, b"name").unwrap_or_default(),
889                ));
890            }
891            Event::End(end) if end.local_name().as_ref() == b"tableStyle" => {
892                if let Some(style) = current_table_style.take() {
893                    table_styles.push(style);
894                }
895            }
896            Event::Empty(start)
897                if current_table_style.is_some()
898                    && start.local_name().as_ref() == b"tableStyleElement" =>
899            {
900                if let Some(element_type) = raw_attribute(&start, b"type")
901                    .as_deref()
902                    .and_then(parse_table_style_element_type)
903                {
904                    let dxf_id = raw_attribute(&start, b"dxfId")
905                        .and_then(|value| value.parse::<usize>().ok());
906                    let format = dxf_id
907                        .and_then(|index| dxfs.get(index).cloned())
908                        .unwrap_or_default();
909                    if let Some(style) = current_table_style.as_mut() {
910                        style
911                            .elements
912                            .push(TableStyleElement::new(element_type, format));
913                    }
914                }
915            }
916
917            Event::Empty(start) if !in_dxfs && start.local_name().as_ref() == b"numFmt" => {
918                let id =
919                    raw_attribute(&start, b"numFmtId").and_then(|value| value.parse::<u32>().ok());
920                let code = raw_attribute(&start, b"formatCode");
921                if let (Some(id), Some(code)) = (id, code) {
922                    num_fmts.insert(id, code);
923                }
924            }
925
926            Event::Start(start) if !in_dxfs && start.local_name().as_ref() == b"font" => {
927                current_font = Some(RawFont {
928                    bold: false,
929                    italic: false,
930                    size: 11.0,
931                    color: None,
932                    name: None,
933                    underline: false,
934                    strike: false,
935                });
936            }
937            Event::End(end) if !in_dxfs && end.local_name().as_ref() == b"font" => {
938                if let Some(font) = current_font.take() {
939                    fonts.push(font);
940                }
941            }
942            Event::Empty(start)
943                if current_font.is_some() && start.local_name().as_ref() == b"b" =>
944            {
945                if let Some(font) = current_font.as_mut() {
946                    font.bold = true;
947                }
948            }
949            Event::Empty(start)
950                if current_font.is_some() && start.local_name().as_ref() == b"i" =>
951            {
952                if let Some(font) = current_font.as_mut() {
953                    font.italic = true;
954                }
955            }
956            // `strike`/`u`/`name`.
957            Event::Empty(start)
958                if current_font.is_some() && start.local_name().as_ref() == b"strike" =>
959            {
960                if let Some(font) = current_font.as_mut() {
961                    font.strike = true;
962                }
963            }
964            Event::Empty(start)
965                if current_font.is_some() && start.local_name().as_ref() == b"u" =>
966            {
967                if let Some(font) = current_font.as_mut() {
968                    font.underline = true;
969                }
970            }
971            Event::Empty(start)
972                if current_font.is_some() && start.local_name().as_ref() == b"name" =>
973            {
974                if let Some(font) = current_font.as_mut() {
975                    font.name = raw_attribute(&start, b"val");
976                }
977            }
978            Event::Empty(start)
979                if current_font.is_some() && start.local_name().as_ref() == b"sz" =>
980            {
981                if let Some(size) =
982                    raw_attribute(&start, b"val").and_then(|value| value.parse::<f64>().ok())
983                {
984                    if let Some(font) = current_font.as_mut() {
985                        font.size = size;
986                    }
987                }
988            }
989            Event::Empty(start)
990                if current_font.is_some() && start.local_name().as_ref() == b"color" =>
991            {
992                if let Some(font) = current_font.as_mut() {
993                    font.color = resolve_color(&start);
994                }
995            }
996
997            Event::Start(start) if !in_dxfs && start.local_name().as_ref() == b"fill" => {
998                in_fill = true;
999                current_fill_pattern_type = None;
1000                current_fill_fg_color = None;
1001                current_fill_bg_color = None;
1002                current_fill_is_gradient = false;
1003                current_gradient_angle = None;
1004                current_gradient_stops.clear();
1005            }
1006            Event::End(end) if !in_dxfs && end.local_name().as_ref() == b"fill" => {
1007                in_fill = false;
1008                let fill = if current_fill_is_gradient {
1009                    RawFill::Gradient(GradientFill {
1010                        angle: current_gradient_angle.unwrap_or(0.0),
1011                        stops: std::mem::take(&mut current_gradient_stops),
1012                    })
1013                } else {
1014                    match current_fill_pattern_type.take().as_deref() {
1015                        Some("none") => RawFill::None,
1016                        Some("gray125") => RawFill::Gray125,
1017                        Some("solid") => match current_fill_fg_color.take() {
1018                            Some(color) => RawFill::Solid(color),
1019                            None => RawFill::Other,
1020                        },
1021                        Some(other) => match parse_pattern_type(other) {
1022                            Some(pattern_type) => RawFill::Pattern(PatternFill {
1023                                pattern_type,
1024                                fg_color: current_fill_fg_color.take(),
1025                                bg_color: current_fill_bg_color.take(),
1026                            }),
1027                            None => RawFill::Other,
1028                        },
1029                        None => RawFill::Other,
1030                    }
1031                };
1032                fills.push(fill);
1033            }
1034            Event::Start(start) if in_fill && start.local_name().as_ref() == b"patternFill" => {
1035                current_fill_pattern_type = raw_attribute(&start, b"patternType");
1036            }
1037            Event::Empty(start) if in_fill && start.local_name().as_ref() == b"patternFill" => {
1038                current_fill_pattern_type = raw_attribute(&start, b"patternType");
1039            }
1040            Event::Empty(start) if in_fill && start.local_name().as_ref() == b"fgColor" => {
1041                current_fill_fg_color = resolve_color(&start);
1042            }
1043            Event::Empty(start) if in_fill && start.local_name().as_ref() == b"bgColor" => {
1044                current_fill_bg_color = resolve_color(&start);
1045            }
1046            // `<gradientFill>`/`<stop>`.
1047            Event::Start(start) if in_fill && start.local_name().as_ref() == b"gradientFill" => {
1048                current_fill_is_gradient = true;
1049                current_gradient_angle =
1050                    raw_attribute(&start, b"degree").and_then(|value| value.parse::<f64>().ok());
1051            }
1052            Event::Start(start)
1053                if in_fill
1054                    && current_fill_is_gradient
1055                    && start.local_name().as_ref() == b"stop" =>
1056            {
1057                current_gradient_stop_position =
1058                    raw_attribute(&start, b"position").and_then(|value| value.parse::<f64>().ok());
1059            }
1060            Event::Empty(start)
1061                if in_fill
1062                    && current_fill_is_gradient
1063                    && start.local_name().as_ref() == b"color" =>
1064            {
1065                if let (Some(position), Some(color)) =
1066                    (current_gradient_stop_position.take(), resolve_color(&start))
1067                {
1068                    current_gradient_stops.push(GradientStop::new(position, color));
1069                }
1070            }
1071
1072            // `<borders>`/`<border>`. Kept outside `in_dxfs` (a `<dxf>` never carries its own
1073            // border, see `CellFormat.border`'s doc comment).
1074            Event::Start(start) if !in_dxfs && start.local_name().as_ref() == b"borders" => {
1075                in_borders = true;
1076            }
1077            Event::End(end) if end.local_name().as_ref() == b"borders" => {
1078                in_borders = false;
1079            }
1080            Event::Start(start) if in_borders && start.local_name().as_ref() == b"border" => {
1081                current_border = Some(RawBorder {
1082                    diagonal_up: raw_attribute(&start, b"diagonalUp").as_deref() == Some("1"),
1083                    diagonal_down: raw_attribute(&start, b"diagonalDown").as_deref() == Some("1"),
1084                    ..RawBorder::default()
1085                });
1086            }
1087            Event::End(end) if in_borders && end.local_name().as_ref() == b"border" => {
1088                if let Some(border) = current_border.take() {
1089                    borders.push(border);
1090                }
1091            }
1092            Event::Start(start)
1093                if current_border.is_some()
1094                    && matches!(
1095                        start.local_name().as_ref(),
1096                        b"left" | b"right" | b"top" | b"bottom" | b"diagonal"
1097                    ) =>
1098            {
1099                current_border_edge_name = Some(match start.local_name().as_ref() {
1100                    b"left" => "left",
1101                    b"right" => "right",
1102                    b"top" => "top",
1103                    b"bottom" => "bottom",
1104                    _ => "diagonal",
1105                });
1106                current_border_edge_style = raw_attribute(&start, b"style");
1107                current_border_edge_color = None;
1108            }
1109            Event::Empty(start)
1110                if current_border.is_some()
1111                    && matches!(
1112                        start.local_name().as_ref(),
1113                        b"left" | b"right" | b"top" | b"bottom" | b"diagonal"
1114                    ) =>
1115            {
1116                // A self-closing empty edge (`<left/>`) — no line on that side, nothing to record.
1117            }
1118            Event::Empty(start)
1119                if current_border_edge_name.is_some()
1120                    && start.local_name().as_ref() == b"color" =>
1121            {
1122                current_border_edge_color = resolve_color(&start);
1123            }
1124            Event::End(end)
1125                if current_border_edge_name.is_some()
1126                    && matches!(
1127                        end.local_name().as_ref(),
1128                        b"left" | b"right" | b"top" | b"bottom" | b"diagonal"
1129                    ) =>
1130            {
1131                if let (Some(name), Some(style), Some(border)) = (
1132                    current_border_edge_name.take(),
1133                    current_border_edge_style.take(),
1134                    current_border.as_mut(),
1135                ) {
1136                    let edge = Some((style, current_border_edge_color.take()));
1137                    match name {
1138                        "left" => border.left = edge,
1139                        "right" => border.right = edge,
1140                        "top" => border.top = edge,
1141                        "bottom" => border.bottom = edge,
1142                        _ => border.diagonal = edge,
1143                    }
1144                }
1145                current_border_edge_color = None;
1146            }
1147
1148            Event::Start(start) if start.local_name().as_ref() == b"cellXfs" => {
1149                in_cell_xfs = true;
1150            }
1151            Event::End(end) if end.local_name().as_ref() == b"cellXfs" => {
1152                in_cell_xfs = false;
1153            }
1154            Event::Empty(start) if in_cell_xfs && start.local_name().as_ref() == b"xf" => {
1155                xfs.push(raw_xf_from_attributes(&start));
1156            }
1157            Event::Start(start) if in_cell_xfs && start.local_name().as_ref() == b"xf" => {
1158                current_xf = Some(raw_xf_from_attributes(&start));
1159            }
1160            Event::End(end) if in_cell_xfs && end.local_name().as_ref() == b"xf" => {
1161                if let Some(xf) = current_xf.take() {
1162                    xfs.push(xf);
1163                }
1164            }
1165            Event::Empty(start)
1166                if in_cell_xfs
1167                    && current_xf.is_some()
1168                    && start.local_name().as_ref() == b"alignment" =>
1169            {
1170                if let Some(xf) = current_xf.as_mut() {
1171                    xf.horizontal = raw_attribute(&start, b"horizontal");
1172                    xf.vertical = raw_attribute(&start, b"vertical");
1173                    xf.wrap_text = raw_attribute(&start, b"wrapText").as_deref() == Some("1");
1174                    xf.indent = raw_attribute(&start, b"indent")
1175                        .and_then(|value| value.parse::<u32>().ok());
1176                    xf.text_rotation = raw_attribute(&start, b"textRotation")
1177                        .and_then(|value| value.parse::<u16>().ok());
1178                    xf.shrink_to_fit =
1179                        raw_attribute(&start, b"shrinkToFit").as_deref() == Some("1");
1180                }
1181            }
1182            // `<protection>`.
1183            Event::Empty(start)
1184                if in_cell_xfs
1185                    && current_xf.is_some()
1186                    && start.local_name().as_ref() == b"protection" =>
1187            {
1188                if let Some(xf) = current_xf.as_mut() {
1189                    xf.locked = raw_attribute(&start, b"locked").map(|value| value == "1");
1190                    xf.formula_hidden = raw_attribute(&start, b"hidden").map(|value| value == "1");
1191                }
1192            }
1193
1194            // `<cellStyleXfs>`/`<xf>`. An exact mirror of the `<cellXfs>`/`<xf>` handling just
1195            // above, guarded by `in_cell_style_xfs` instead of `in_cell_xfs` so the two `<xf>`
1196            // collections stay separate.
1197            Event::Start(start) if start.local_name().as_ref() == b"cellStyleXfs" => {
1198                in_cell_style_xfs = true;
1199            }
1200            Event::End(end) if end.local_name().as_ref() == b"cellStyleXfs" => {
1201                in_cell_style_xfs = false;
1202            }
1203            Event::Empty(start) if in_cell_style_xfs && start.local_name().as_ref() == b"xf" => {
1204                style_xfs.push(raw_xf_from_attributes(&start));
1205            }
1206            Event::Start(start) if in_cell_style_xfs && start.local_name().as_ref() == b"xf" => {
1207                current_style_xf = Some(raw_xf_from_attributes(&start));
1208            }
1209            Event::End(end) if in_cell_style_xfs && end.local_name().as_ref() == b"xf" => {
1210                if let Some(xf) = current_style_xf.take() {
1211                    style_xfs.push(xf);
1212                }
1213            }
1214            Event::Empty(start)
1215                if in_cell_style_xfs
1216                    && current_style_xf.is_some()
1217                    && start.local_name().as_ref() == b"alignment" =>
1218            {
1219                if let Some(xf) = current_style_xf.as_mut() {
1220                    xf.horizontal = raw_attribute(&start, b"horizontal");
1221                    xf.vertical = raw_attribute(&start, b"vertical");
1222                    xf.wrap_text = raw_attribute(&start, b"wrapText").as_deref() == Some("1");
1223                    xf.indent = raw_attribute(&start, b"indent")
1224                        .and_then(|value| value.parse::<u32>().ok());
1225                    xf.text_rotation = raw_attribute(&start, b"textRotation")
1226                        .and_then(|value| value.parse::<u16>().ok());
1227                    xf.shrink_to_fit =
1228                        raw_attribute(&start, b"shrinkToFit").as_deref() == Some("1");
1229                }
1230            }
1231            Event::Empty(start)
1232                if in_cell_style_xfs
1233                    && current_style_xf.is_some()
1234                    && start.local_name().as_ref() == b"protection" =>
1235            {
1236                if let Some(xf) = current_style_xf.as_mut() {
1237                    xf.locked = raw_attribute(&start, b"locked").map(|value| value == "1");
1238                    xf.formula_hidden = raw_attribute(&start, b"hidden").map(|value| value == "1");
1239                }
1240            }
1241
1242            // `<cellStyles>`/`<cellStyle name=".." xfId=".." builtinId="..">` — point 58. Kept in
1243            // document order; the implicit `"Normal"` entry (`xfId="0"`) is filtered out when
1244            // building `named_cell_styles` below, not here (this crate is tolerant of a real file's
1245            // `<cellStyle>` list appearing in any order).
1246            Event::Empty(start) | Event::Start(start)
1247                if start.local_name().as_ref() == b"cellStyle" =>
1248            {
1249                if let Some(name) = raw_attribute(&start, b"name") {
1250                    let xf_id = raw_attribute(&start, b"xfId")
1251                        .and_then(|value| value.parse::<usize>().ok())
1252                        .unwrap_or(0);
1253                    let builtin_id = raw_attribute(&start, b"builtinId")
1254                        .and_then(|value| value.parse::<u32>().ok());
1255                    cell_styles.push((name, xf_id, builtin_id));
1256                }
1257            }
1258
1259            Event::Start(start) if start.local_name().as_ref() == b"dxfs" => {
1260                in_dxfs = true;
1261            }
1262            Event::End(end) if end.local_name().as_ref() == b"dxfs" => {
1263                in_dxfs = false;
1264            }
1265            Event::Start(start) if in_dxfs && start.local_name().as_ref() == b"dxf" => {
1266                current_dxf = Some(CellFormat::default());
1267            }
1268            Event::End(end) if in_dxfs && end.local_name().as_ref() == b"dxf" => {
1269                if let Some(dxf) = current_dxf.take() {
1270                    dxfs.push(dxf);
1271                }
1272            }
1273
1274            Event::Start(start)
1275                if in_dxfs && current_dxf.is_some() && start.local_name().as_ref() == b"font" =>
1276            {
1277                in_dxf_font = true;
1278                dxf_font_bold = false;
1279                dxf_font_italic = false;
1280                dxf_font_color = None;
1281            }
1282            Event::End(end) if in_dxfs && end.local_name().as_ref() == b"font" => {
1283                in_dxf_font = false;
1284                if let Some(dxf) = current_dxf.as_mut() {
1285                    dxf.bold = dxf_font_bold;
1286                    dxf.italic = dxf_font_italic;
1287                    dxf.font_color = dxf_font_color.take();
1288                }
1289            }
1290            Event::Empty(start) if in_dxf_font && start.local_name().as_ref() == b"b" => {
1291                dxf_font_bold = true;
1292            }
1293            Event::Empty(start) if in_dxf_font && start.local_name().as_ref() == b"i" => {
1294                dxf_font_italic = true;
1295            }
1296            Event::Empty(start) if in_dxf_font && start.local_name().as_ref() == b"color" => {
1297                dxf_font_color = raw_attribute(&start, b"rgb");
1298            }
1299
1300            Event::Empty(start)
1301                if in_dxfs && current_dxf.is_some() && start.local_name().as_ref() == b"numFmt" =>
1302            {
1303                let code = raw_attribute(&start, b"formatCode");
1304                if let Some(dxf) = current_dxf.as_mut() {
1305                    dxf.number_format = code;
1306                }
1307            }
1308
1309            Event::Start(start)
1310                if in_dxfs && current_dxf.is_some() && start.local_name().as_ref() == b"fill" =>
1311            {
1312                in_dxf_fill = true;
1313                dxf_fill_pattern_type = None;
1314                dxf_fill_bg_color = None;
1315            }
1316            Event::End(end) if in_dxfs && end.local_name().as_ref() == b"fill" => {
1317                in_dxf_fill = false;
1318                if dxf_fill_pattern_type.as_deref() == Some("solid") {
1319                    if let Some(dxf) = current_dxf.as_mut() {
1320                        dxf.fill_color = dxf_fill_bg_color.take();
1321                    }
1322                }
1323            }
1324            Event::Start(start) if in_dxf_fill && start.local_name().as_ref() == b"patternFill" => {
1325                dxf_fill_pattern_type = raw_attribute(&start, b"patternType");
1326            }
1327            Event::Empty(start) if in_dxf_fill && start.local_name().as_ref() == b"patternFill" => {
1328                dxf_fill_pattern_type = raw_attribute(&start, b"patternType");
1329            }
1330            // A dxf's solid fill color is declared as `<bgColor>`, not `<fgColor>` — see research
1331            // notes (`CT_Dxf`'s doc comment in `writer.rs`).
1332            Event::Empty(start) if in_dxf_fill && start.local_name().as_ref() == b"bgColor" => {
1333                dxf_fill_bg_color = raw_attribute(&start, b"rgb");
1334            }
1335
1336            Event::Empty(start)
1337                if in_dxfs
1338                    && current_dxf.is_some()
1339                    && start.local_name().as_ref() == b"alignment" =>
1340            {
1341                if let Some(dxf) = current_dxf.as_mut() {
1342                    dxf.horizontal_alignment = raw_attribute(&start, b"horizontal")
1343                        .as_deref()
1344                        .and_then(parse_horizontal_alignment);
1345                    dxf.vertical_alignment = raw_attribute(&start, b"vertical")
1346                        .as_deref()
1347                        .and_then(parse_vertical_alignment);
1348                }
1349            }
1350
1351            _ => {}
1352        }
1353    }
1354
1355    // `xfId -> name`, from every non-`"Normal"` `<cellStyle>` entry (`xfId` `0` is always the
1356    // implicit "Normal" base, same convention this crate's own writer uses — see `NamedCellStyle`'s
1357    // doc comment).
1358    let style_name_by_xf_id: HashMap<usize, String> = cell_styles
1359        .iter()
1360        .filter(|(_, xf_id, _)| *xf_id != 0)
1361        .map(|(name, xf_id, _)| (*xf_id, name.clone()))
1362        .collect();
1363
1364    let cell_formats = (0..xfs.len())
1365        .map(|index| {
1366            resolve_cell_format(
1367                index,
1368                &xfs,
1369                &fonts,
1370                &fills,
1371                &borders,
1372                &num_fmts,
1373                &style_name_by_xf_id,
1374            )
1375        })
1376        .collect();
1377
1378    // `named_cell_styles` — built from `cell_styles` (`<cellStyle>`, in document order), each
1379    // resolved against its own declared `xfId` into `style_xfs` (`<cellStyleXfs>`). The implicit
1380    // `"Normal"` entry (`xfId="0"`) is skipped, matching how this crate's own writer always
1381    // reserves index `0` for it and never exposes it as a `NamedCellStyle`.
1382    let named_cell_styles: Vec<NamedCellStyle> = cell_styles
1383        .iter()
1384        .filter(|(_, xf_id, _)| *xf_id != 0)
1385        .filter_map(|(name, xf_id, builtin_id)| {
1386            let xf = style_xfs.get(*xf_id)?;
1387            let format = resolve_cell_format_raw(xf, &fonts, &fills, &borders, &num_fmts, None);
1388            let mut style = NamedCellStyle::new(name.clone(), format);
1389            if let Some(builtin_id) = builtin_id {
1390                style = style.with_builtin_id(*builtin_id);
1391            }
1392            Some(style)
1393        })
1394        .collect();
1395
1396    Ok(ParsedStyles {
1397        cell_formats,
1398        dxfs,
1399        table_styles,
1400        named_cell_styles,
1401    })
1402}
1403
1404/// Maps `ST_TableStyleType`'s token back to a [`TableStyleElementType`] — the reader-side
1405/// counterpart of `writer.rs::table_style_element_type_str`. `None` for any of the schema's
1406/// remaining, not-modeled values (see `TableStyleElementType`'s own doc comment) — that
1407/// `<tableStyleElement>` is simply skipped, same "tolerate but don't model everything a real file
1408/// might contain" posture as elsewhere in this crate's reader.
1409fn parse_table_style_element_type(value: &str) -> Option<TableStyleElementType> {
1410    match value {
1411        "wholeTable" => Some(TableStyleElementType::WholeTable),
1412        "headerRow" => Some(TableStyleElementType::HeaderRow),
1413        "totalRow" => Some(TableStyleElementType::TotalRow),
1414        "firstRowStripe" => Some(TableStyleElementType::FirstRowStripe),
1415        "secondRowStripe" => Some(TableStyleElementType::SecondRowStripe),
1416        "firstColumnStripe" => Some(TableStyleElementType::FirstColumnStripe),
1417        "secondColumnStripe" => Some(TableStyleElementType::SecondColumnStripe),
1418        "firstColumn" => Some(TableStyleElementType::FirstColumn),
1419        "lastColumn" => Some(TableStyleElementType::LastColumn),
1420        _ => None,
1421    }
1422}
1423
1424fn raw_xf_from_attributes(start: &BytesStart) -> RawXf {
1425    RawXf {
1426        num_fmt_id: raw_attribute(start, b"numFmtId")
1427            .and_then(|value| value.parse::<u32>().ok())
1428            .unwrap_or(0),
1429        font_id: raw_attribute(start, b"fontId")
1430            .and_then(|value| value.parse::<usize>().ok())
1431            .unwrap_or(0),
1432        fill_id: raw_attribute(start, b"fillId")
1433            .and_then(|value| value.parse::<usize>().ok())
1434            .unwrap_or(0),
1435        border_id: raw_attribute(start, b"borderId")
1436            .and_then(|value| value.parse::<usize>().ok())
1437            .unwrap_or(0),
1438        xf_id: raw_attribute(start, b"xfId")
1439            .and_then(|value| value.parse::<usize>().ok())
1440            .unwrap_or(0),
1441        horizontal: None,
1442        vertical: None,
1443        wrap_text: false,
1444        indent: None,
1445        text_rotation: None,
1446        shrink_to_fit: false,
1447        locked: None,
1448        formula_hidden: None,
1449    }
1450}
1451
1452/// Excel's legacy 56-color indexed palette (`ST_ColorScheme`'s pre-theme era, referenced via
1453/// `<color indexed="N">` instead of `rgb`/`theme`). This is the same well-known fixed table every
1454/// spreadsheet library reproduces, reproduced here from memory of that widely-published table
1455/// rather than copied from any one source — **not verified against a real Excel file**. Indices
1456/// 64/65 (`ST_SystemColorVal`'s "system foreground"/"system background", not really part of the
1457/// 56-color table but sometimes seen in the same attribute) map to black/white. An out-of-range
1458/// index resolves to `None` (no color recorded) rather than a guess.
1459fn indexed_color(index: u32) -> Option<&'static str> {
1460    const PALETTE: [&str; 64] = [
1461        "FF000000", "FFFFFFFF", "FFFF0000", "FF00FF00", "FF0000FF", "FFFFFF00", "FFFF00FF",
1462        "FF00FFFF", "FF000000", "FFFFFFFF", "FFFF0000", "FF00FF00", "FF0000FF", "FFFFFF00",
1463        "FFFF00FF", "FF00FFFF", "FF800000", "FF008000", "FF000080", "FF808000", "FF800080",
1464        "FF008080", "FFC0C0C0", "FF808080", "FF9999FF", "FF993366", "FFFFFFCC", "FFCCFFFF",
1465        "FF660066", "FFFF8080", "FF0066CC", "FFCCCCFF", "FF000080", "FFFF00FF", "FFFFFF00",
1466        "FF00FFFF", "FF800080", "FF800000", "FF008080", "FF0000FF", "FF00CCFF", "FFCCFFFF",
1467        "FFCCFFCC", "FFFFFF99", "FF99CCFF", "FFFF99CC", "FFCC99FF", "FFFFCC99", "FF3366FF",
1468        "FF33CCCC", "FF99CC00", "FFFFCC00", "FFFF9900", "FFFF6600", "FF666699", "FF969696",
1469        "FF003366", "FF339966", "FF003300", "FF333300", "FF993300", "FF993366", "FF333399",
1470        "FF333333",
1471    ];
1472    match index {
1473        0..=63 => Some(PALETTE[index as usize]),
1474        64 => Some("FF000000"),
1475        65 => Some("FFFFFFFF"),
1476        _ => None,
1477    }
1478}
1479
1480/// Resolves a color-bearing element (`<color>`/`<fgColor>`/`<bgColor>`, or a border edge's own
1481/// `<color>`) to an 8-hex-digit ARGB string, preferring an explicit `rgb` attribute and falling
1482/// back to `indexed` — a `theme`-referenced color (this crate never writes one, see
1483/// `to_theme_xml`'s doc comment) is not resolved.
1484fn resolve_color(start: &BytesStart) -> Option<String> {
1485    if let Some(rgb) = raw_attribute(start, b"rgb") {
1486        return Some(rgb);
1487    }
1488    raw_attribute(start, b"indexed")
1489        .and_then(|value| value.parse::<u32>().ok())
1490        .and_then(indexed_color)
1491        .map(|value| value.to_string())
1492}
1493
1494/// Maps an ECMA-376 `ST_PatternType` token back to a [`PatternType`] — the inverse of
1495/// `writer.rs::pattern_type_str`. `"none"`/`"gray125"`/ `"solid"` are handled separately by the
1496/// caller (see `RawFill`'s doc comment), so only the remaining pattern tokens are mapped here.
1497fn parse_pattern_type(value: &str) -> Option<PatternType> {
1498    match value {
1499        "gray0625" => Some(PatternType::Gray0625),
1500        "darkGray" => Some(PatternType::DarkGray),
1501        "mediumGray" => Some(PatternType::MediumGray),
1502        "lightGray" => Some(PatternType::LightGray),
1503        "darkHorizontal" => Some(PatternType::DarkHorizontal),
1504        "darkVertical" => Some(PatternType::DarkVertical),
1505        "darkDown" => Some(PatternType::DarkDown),
1506        "darkUp" => Some(PatternType::DarkUp),
1507        "darkGrid" => Some(PatternType::DarkGrid),
1508        "darkTrellis" => Some(PatternType::DarkTrellis),
1509        "lightHorizontal" => Some(PatternType::LightHorizontal),
1510        "lightVertical" => Some(PatternType::LightVertical),
1511        "lightDown" => Some(PatternType::LightDown),
1512        "lightUp" => Some(PatternType::LightUp),
1513        "lightGrid" => Some(PatternType::LightGrid),
1514        "lightTrellis" => Some(PatternType::LightTrellis),
1515        _ => None,
1516    }
1517}
1518
1519/// Maps an ECMA-376 `ST_BorderStyle` token back to a [`BorderStyle`] — the inverse of
1520/// `writer.rs::border_style_str`. Any style outside this crate's modeled subset (see
1521/// `BorderStyle`'s doc comment) resolves to `None`, so that edge reads back as "no border" rather
1522/// than an incorrectly-substituted style.
1523fn parse_border_style(value: &str) -> Option<BorderStyle> {
1524    match value {
1525        "thin" => Some(BorderStyle::Thin),
1526        "medium" => Some(BorderStyle::Medium),
1527        "thick" => Some(BorderStyle::Thick),
1528        "dashed" => Some(BorderStyle::Dashed),
1529        "dotted" => Some(BorderStyle::Dotted),
1530        "double" => Some(BorderStyle::Double),
1531        "hair" => Some(BorderStyle::Hair),
1532        _ => None,
1533    }
1534}
1535
1536/// Resolves one `RawBorder` (by index in `borders`) into a [`Border`] — `None`/out-of-range
1537/// resolves to `Border::default()` (no borders at all), same forgiving posture as
1538/// `resolve_cell_format`'s own out-of-range handling.
1539fn resolve_border(border_id: usize, borders: &[RawBorder]) -> Border {
1540    let Some(raw) = borders.get(border_id) else {
1541        return Border::default();
1542    };
1543    let resolve_edge = |edge: &Option<(String, Option<String>)>| -> Option<BorderEdge> {
1544        edge.as_ref().and_then(|(style, color)| {
1545            parse_border_style(style).map(|style| {
1546                let mut edge = BorderEdge::new(style);
1547                if let Some(color) = color {
1548                    edge = edge.with_color(color.clone());
1549                }
1550                edge
1551            })
1552        })
1553    };
1554    Border {
1555        top: resolve_edge(&raw.top),
1556        bottom: resolve_edge(&raw.bottom),
1557        left: resolve_edge(&raw.left),
1558        right: resolve_edge(&raw.right),
1559        diagonal: resolve_edge(&raw.diagonal),
1560        diagonal_up: raw.diagonal_up,
1561        diagonal_down: raw.diagonal_down,
1562    }
1563}
1564
1565fn parse_horizontal_alignment(value: &str) -> Option<HorizontalAlignment> {
1566    match value {
1567        "left" => Some(HorizontalAlignment::Left),
1568        "center" => Some(HorizontalAlignment::Center),
1569        "right" => Some(HorizontalAlignment::Right),
1570        _ => None,
1571    }
1572}
1573
1574/// Maps an ECMA-376 `ST_Orientation` token back to a [`PageOrientation`] — the inverse of
1575/// `writer.rs::page_orientation_str`. `"default"` (a third schema value Excel treats as portrait)
1576/// resolves to `None`, same posture as `PageOrientation`'s own doc comment.
1577fn parse_page_orientation(value: &str) -> Option<PageOrientation> {
1578    match value {
1579        "portrait" => Some(PageOrientation::Portrait),
1580        "landscape" => Some(PageOrientation::Landscape),
1581        _ => None,
1582    }
1583}
1584
1585fn parse_vertical_alignment(value: &str) -> Option<VerticalAlignment> {
1586    match value {
1587        "top" => Some(VerticalAlignment::Top),
1588        "center" => Some(VerticalAlignment::Center),
1589        "bottom" => Some(VerticalAlignment::Bottom),
1590        _ => None,
1591    }
1592}
1593
1594/// Maps an ECMA-376 comparison operator token back to a [`ComparisonOperator`] — the inverse of
1595/// `writer.rs`'s `comparison_operator_str`, shared by both `cfRule@operator` and
1596/// `dataValidation@operator` (see `ComparisonOperator`'s own doc comment for why one enum covers
1597/// both).
1598fn parse_comparison_operator(value: &str) -> Option<ComparisonOperator> {
1599    match value {
1600        "lessThan" => Some(ComparisonOperator::LessThan),
1601        "lessThanOrEqual" => Some(ComparisonOperator::LessThanOrEqual),
1602        "equal" => Some(ComparisonOperator::Equal),
1603        "notEqual" => Some(ComparisonOperator::NotEqual),
1604        "greaterThanOrEqual" => Some(ComparisonOperator::GreaterThanOrEqual),
1605        "greaterThan" => Some(ComparisonOperator::GreaterThan),
1606        "between" => Some(ComparisonOperator::Between),
1607        "notBetween" => Some(ComparisonOperator::NotBetween),
1608        _ => None,
1609    }
1610}
1611
1612/// Maps a `cfRule@type` text-comparison token (`containsText`/
1613/// `notContainsText`/`beginsWith`/`endsWith`) back to a [`TextComparisonOperator`] — the inverse of
1614/// `writer.rs`'s `text_comparison_operator_type_str`.
1615fn text_comparison_operator_from_type(value: &str) -> Option<TextComparisonOperator> {
1616    match value {
1617        "containsText" => Some(TextComparisonOperator::Contains),
1618        "notContainsText" => Some(TextComparisonOperator::NotContains),
1619        "beginsWith" => Some(TextComparisonOperator::BeginsWith),
1620        "endsWith" => Some(TextComparisonOperator::EndsWith),
1621        _ => None,
1622    }
1623}
1624
1625/// Resolves a single `cellXfs` entry (by index) into an `Option<CellFormat>` — `None` for index 0
1626/// (Excel's mandatory "Normal" baseline style, by convention always the default/unformatted style —
1627/// a safe assumption for any real `.xlsx`, not just this crate's own output) or an out-of-range
1628/// index; `Some(CellFormat)` otherwise, with its font/fill looked up from the parsed
1629/// `fonts`/`fills` collections and its number format looked up from `num_fmts` (only custom,
1630/// declared codes — see `parse_styles_xml`'s doc comment).
1631fn resolve_cell_format(
1632    xf_index: usize,
1633    xfs: &[RawXf],
1634    fonts: &[RawFont],
1635    fills: &[RawFill],
1636    borders: &[RawBorder],
1637    num_fmts: &HashMap<u32, String>,
1638    style_name_by_xf_id: &HashMap<usize, String>,
1639) -> Option<CellFormat> {
1640    if xf_index == 0 {
1641        return None;
1642    }
1643    let xf = xfs.get(xf_index)?;
1644    // The point 58: resolves this entry's own `xfId` (an index into `cellStyleXfs`) back into a
1645    // named style, via whichever `<cellStyle>` declared that same `xfId` — see
1646    // `CellFormat::named_style`'s doc comment.
1647    let named_style = style_name_by_xf_id.get(&xf.xf_id).cloned();
1648    Some(resolve_cell_format_raw(
1649        xf,
1650        fonts,
1651        fills,
1652        borders,
1653        num_fmts,
1654        named_style,
1655    ))
1656}
1657
1658/// The shared resolution body behind [`resolve_cell_format`] (for a `cellXfs` entry) and
1659/// `named_cell_styles`' own construction (for a `cellStyleXfs` entry, point 58) — unlike
1660/// `resolve_cell_format`, this takes the [`RawXf`] directly rather than an index, and has no "index
1661/// 0 resolves to nothing" special case (that's specific to `cellXfs`'s own index space, not
1662/// meaningful for `cellStyleXfs`).
1663fn resolve_cell_format_raw(
1664    xf: &RawXf,
1665    fonts: &[RawFont],
1666    fills: &[RawFill],
1667    borders: &[RawBorder],
1668    num_fmts: &HashMap<u32, String>,
1669    named_style: Option<String>,
1670) -> CellFormat {
1671    let number_format = num_fmts.get(&xf.num_fmt_id).cloned();
1672
1673    let font = fonts.get(xf.font_id);
1674    let bold = font.map(|font| font.bold).unwrap_or(false);
1675    let italic = font.map(|font| font.italic).unwrap_or(false);
1676    let underline = font.map(|font| font.underline).unwrap_or(false);
1677    let strike = font.map(|font| font.strike).unwrap_or(false);
1678    // Font 0 is always the workbook default — treat it as "no override" so a format that never
1679    // touched the font at all reads back with `font_size`/`font_color`/`font_name` as `None`, not
1680    // the resolved default values.
1681    let (font_size, font_color, font_name) = if xf.font_id == 0 {
1682        (None, None, None)
1683    } else {
1684        (
1685            font.map(|font| font.size),
1686            font.and_then(|font| font.color.clone()),
1687            font.and_then(|font| font.name.clone()),
1688        )
1689    };
1690
1691    // Gradient/pattern fills are separate `CellFormat` fields from `fill_color` (plain solid) — see
1692    // `CellFormat::pattern_fill`'s doc comment.
1693    let (fill_color, pattern_fill, gradient_fill) = match fills.get(xf.fill_id) {
1694        Some(RawFill::Solid(color)) => (Some(color.clone()), None, None),
1695        Some(RawFill::Pattern(pattern)) => (None, Some(pattern.clone()), None),
1696        Some(RawFill::Gradient(gradient)) => (None, None, Some(gradient.clone())),
1697        _ => (None, None, None),
1698    };
1699
1700    let horizontal_alignment = xf
1701        .horizontal
1702        .as_deref()
1703        .and_then(parse_horizontal_alignment);
1704    let vertical_alignment = xf.vertical.as_deref().and_then(parse_vertical_alignment);
1705
1706    CellFormat {
1707        number_format,
1708        bold,
1709        italic,
1710        font_size,
1711        font_color,
1712        fill_color,
1713        horizontal_alignment,
1714        vertical_alignment,
1715        border: resolve_border(xf.border_id, borders),
1716        font_name,
1717        underline,
1718        strike,
1719        wrap_text: xf.wrap_text,
1720        indent: xf.indent,
1721        text_rotation: xf.text_rotation,
1722        shrink_to_fit: xf.shrink_to_fit,
1723        pattern_fill,
1724        gradient_fill,
1725        locked: xf.locked,
1726        formula_hidden: xf.formula_hidden,
1727        named_style,
1728    }
1729}
1730
1731/// The four `docProps/core.xml` fields [`parse_core_properties_xml`] collects — grouped into a
1732/// struct instead of a same-typed 4-tuple (clippy's `type_complexity`; four `Option<String>`s in a
1733/// row is also exactly the kind of positional tuple that's easy to transpose). Named the same as
1734/// `powerpoint-ooxml`'s own equivalent struct — same fields, same reasoning, see that crate's
1735/// `parse_core_properties_xml`.
1736#[derive(Default)]
1737struct ParsedCoreProperties {
1738    title: Option<String>,
1739    author: Option<String>,
1740    subject: Option<String>,
1741    keywords: Option<String>,
1742}
1743
1744/// Parses `docProps/core.xml` into title/author/subject/keywords.
1745fn parse_core_properties_xml(xml: &str) -> Result<ParsedCoreProperties> {
1746    let mut reader = Reader::from_xml_str(xml);
1747    let mut title = None;
1748    let mut author = None;
1749    let mut subject = None;
1750    let mut keywords = None;
1751    let mut in_element: Option<&'static str> = None;
1752    let mut buffer = String::new();
1753
1754    loop {
1755        match reader.read_event()? {
1756            Event::Eof => break,
1757            Event::Start(start) if start.local_name().as_ref() == b"title" => {
1758                in_element = Some("title");
1759                buffer.clear();
1760            }
1761            Event::Start(start) if start.local_name().as_ref() == b"creator" => {
1762                in_element = Some("creator");
1763                buffer.clear();
1764            }
1765            Event::Start(start) if start.local_name().as_ref() == b"subject" => {
1766                in_element = Some("subject");
1767                buffer.clear();
1768            }
1769            Event::Start(start) if start.local_name().as_ref() == b"keywords" => {
1770                in_element = Some("keywords");
1771                buffer.clear();
1772            }
1773            Event::Text(text) if in_element.is_some() => {
1774                buffer.push_str(&xml_core::decode_text(&text)?);
1775            }
1776            Event::GeneralRef(reference) if in_element.is_some() => {
1777                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
1778                    buffer.push_str(&resolved);
1779                }
1780            }
1781            Event::End(end)
1782                if in_element.is_some()
1783                    && matches!(
1784                        end.local_name().as_ref(),
1785                        b"title" | b"creator" | b"subject" | b"keywords"
1786                    ) =>
1787            {
1788                let text = std::mem::take(&mut buffer);
1789                match in_element.take() {
1790                    Some("title") => title = Some(text),
1791                    Some("creator") => author = Some(text),
1792                    Some("subject") => subject = Some(text),
1793                    Some("keywords") => keywords = Some(text),
1794                    _ => {}
1795                }
1796            }
1797            _ => {}
1798        }
1799    }
1800
1801    Ok(ParsedCoreProperties {
1802        title,
1803        author,
1804        subject,
1805        keywords,
1806    })
1807}
1808
1809/// Parses `docProps/app.xml` into `(Company, Manager, HyperlinkBase)` — extended by
1810/// (`Manager`/`HyperlinkBase`).
1811fn parse_extended_properties_xml(
1812    xml: &str,
1813) -> Result<(Option<String>, Option<String>, Option<String>)> {
1814    let mut reader = Reader::from_xml_str(xml);
1815    let mut company = None;
1816    let mut manager = None;
1817    let mut hyperlink_base = None;
1818    let mut in_element: Option<&'static str> = None;
1819    let mut buffer = String::new();
1820
1821    loop {
1822        match reader.read_event()? {
1823            Event::Eof => break,
1824            Event::Start(start) if start.local_name().as_ref() == b"Company" => {
1825                in_element = Some("Company");
1826                buffer.clear();
1827            }
1828            Event::Start(start) if start.local_name().as_ref() == b"Manager" => {
1829                in_element = Some("Manager");
1830                buffer.clear();
1831            }
1832            Event::Start(start) if start.local_name().as_ref() == b"HyperlinkBase" => {
1833                in_element = Some("HyperlinkBase");
1834                buffer.clear();
1835            }
1836            Event::Text(text) if in_element.is_some() => {
1837                buffer.push_str(&xml_core::decode_text(&text)?);
1838            }
1839            Event::GeneralRef(reference) if in_element.is_some() => {
1840                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
1841                    buffer.push_str(&resolved);
1842                }
1843            }
1844            Event::End(end)
1845                if in_element.is_some()
1846                    && matches!(
1847                        end.local_name().as_ref(),
1848                        b"Company" | b"Manager" | b"HyperlinkBase"
1849                    ) =>
1850            {
1851                let text = std::mem::take(&mut buffer);
1852                match in_element.take() {
1853                    Some("Company") => company = Some(text),
1854                    Some("Manager") => manager = Some(text),
1855                    Some("HyperlinkBase") => hyperlink_base = Some(text),
1856                    _ => {}
1857                }
1858            }
1859            _ => {}
1860        }
1861    }
1862
1863    Ok((company, manager, hyperlink_base))
1864}
1865
1866/// Parses `docProps/custom.xml` (OPC custom properties) into `(name, value)` pairs — point 56, an
1867/// exact mirror of `word-ooxml`'s own `parse_custom_properties_xml` (including which 4
1868/// [`CustomPropertyValue`] variants are decoded, in the same document order it already has them in
1869/// ascending `pid` order). Each `<property name="..">` element's single variant-type child
1870/// (`vt:lpwstr`/ `vt:bool`/`vt:i4`/`vt:r8`) is decoded by its own local name; a property whose
1871/// variant type isn't one of these four, or that has no `name` attribute, is silently skipped,
1872/// matching this reader's usual forgiving policy. `fmtid`/`pid` are not validated against
1873/// `CUSTOM_PROPERTY_FMTID`/a specific sequence — same best-effort posture as elsewhere in this
1874/// crate.
1875fn parse_custom_properties_xml(xml: &str) -> Result<Vec<(String, CustomPropertyValue)>> {
1876    let mut reader = Reader::from_xml_str(xml);
1877    let mut custom_properties = Vec::new();
1878    let mut current_name: Option<String> = None;
1879    let mut current_variant: Option<Vec<u8>> = None;
1880    let mut buffer = String::new();
1881
1882    loop {
1883        match reader.read_event()? {
1884            Event::Eof => break,
1885
1886            Event::Start(start) if start.local_name().as_ref() == b"property" => {
1887                current_name = raw_attribute(&start, b"name");
1888            }
1889
1890            Event::Start(start) | Event::Empty(start)
1891                if current_name.is_some()
1892                    && matches!(
1893                        start.local_name().as_ref(),
1894                        b"lpwstr" | b"bool" | b"i4" | b"r8"
1895                    ) =>
1896            {
1897                current_variant = Some(start.local_name().as_ref().to_vec());
1898                buffer.clear();
1899            }
1900
1901            Event::Text(text) if current_variant.is_some() => {
1902                buffer.push_str(&xml_core::decode_text(&text)?);
1903            }
1904
1905            Event::GeneralRef(reference) if current_variant.is_some() => {
1906                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
1907                    buffer.push_str(&resolved);
1908                }
1909            }
1910
1911            Event::End(end) => {
1912                if let Some(variant) = current_variant.take() {
1913                    if end.local_name().as_ref() == variant.as_slice() {
1914                        if let Some(name) = current_name.clone() {
1915                            let value = std::mem::take(&mut buffer);
1916                            let parsed = match variant.as_slice() {
1917                                b"lpwstr" => Some(CustomPropertyValue::Text(value)),
1918                                b"bool" => {
1919                                    Some(CustomPropertyValue::Bool(value == "true" || value == "1"))
1920                                }
1921                                b"i4" => value.parse::<i32>().ok().map(CustomPropertyValue::Int),
1922                                b"r8" => value.parse::<f64>().ok().map(CustomPropertyValue::Number),
1923                                _ => None,
1924                            };
1925                            if let Some(value) = parsed {
1926                                custom_properties.push((name, value));
1927                            }
1928                        }
1929                    } else {
1930                        current_variant = Some(variant);
1931                    }
1932                } else if end.local_name().as_ref() == b"property" {
1933                    current_name = None;
1934                }
1935            }
1936
1937            _ => {}
1938        }
1939    }
1940
1941    Ok(custom_properties)
1942}
1943
1944/// [`parse_external_link_xml`]'s two collections — grouped into a struct instead of a
1945/// tuple-of-`Vec<(String, String)>` (clippy's `type_complexity`).
1946struct ParsedExternalLink {
1947    sheet_names: Vec<String>,
1948    defined_names: Vec<(String, String)>,
1949}
1950
1951/// Parses `xl/externalLinks/externalLinkN.xml` (`CT_ExternalLink`) into its sheet names and defined
1952/// names. `<sheetDataSet>` (cached values) is never read back, see `ExternalWorkbookLink`'s doc
1953/// comment.
1954fn parse_external_link_xml(xml: &str) -> Result<ParsedExternalLink> {
1955    let mut reader = Reader::from_xml_str(xml);
1956    let mut sheet_names = Vec::new();
1957    let mut defined_names = Vec::new();
1958
1959    loop {
1960        match reader.read_event()? {
1961            Event::Eof => break,
1962            Event::Empty(start) if start.local_name().as_ref() == b"sheetName" => {
1963                if let Some(name) = raw_attribute(&start, b"val") {
1964                    sheet_names.push(name);
1965                }
1966            }
1967            Event::Empty(start) if start.local_name().as_ref() == b"definedName" => {
1968                if let (Some(name), Some(refers_to)) = (
1969                    raw_attribute(&start, b"name"),
1970                    raw_attribute(&start, b"refersTo"),
1971                ) {
1972                    defined_names.push((name, refers_to));
1973                }
1974            }
1975            _ => {}
1976        }
1977    }
1978
1979    Ok(ParsedExternalLink {
1980        sheet_names,
1981        defined_names,
1982    })
1983}
1984
1985/// Parses `xl/commentsN.xml` (`CT_Comments`) into a `Vec<CellComment>`. Resolves each `<comment
1986/// authorId="N">`'s author by index into `<authors>`; concatenates every `<t>` inside a comment's
1987/// `<text>` regardless of run nesting (mirrors `parse_shared_strings_xml`'s own "don't model
1988/// per-run formatting" posture for a comment body, which this crate's own `CellComment` never
1989/// modeled as rich text to begin with — see its doc comment). Parses `xl/commentsN.xml`'s
1990/// `<comment><text>` (`CT_Rst`, the exact same shape as `xl/sharedStrings.xml`'s `<si>` — see
1991/// `parse_shared_strings_xml` for the counterpart this mirrors) into either a plain `text` or, when
1992/// the body carries one or more `<r>` runs, `rich_text: Some(.)` — extended for rich text.
1993fn parse_comments_xml(xml: &str) -> Result<Vec<CellComment>> {
1994    let mut reader = Reader::from_xml_str(xml);
1995    let mut authors: Vec<String> = Vec::new();
1996    let mut in_author = false;
1997    let mut author_buffer = String::new();
1998
1999    let mut comments = Vec::new();
2000    let mut in_comment = false;
2001    let mut current_ref: Option<String> = None;
2002    let mut current_author_id: Option<usize> = None;
2003
2004    let mut in_comment_text = false;
2005    let mut text_is_rich = false;
2006    let mut plain_buffer = String::new();
2007    let mut runs: Vec<RichTextRun> = Vec::new();
2008
2009    let mut in_run = false;
2010    let mut in_rpr = false;
2011    let mut in_text_element = false;
2012    let mut run_text_buffer = String::new();
2013    let mut run_bold = false;
2014    let mut run_italic = false;
2015    let mut run_size: Option<f64> = None;
2016    let mut run_color: Option<String> = None;
2017    let mut run_name: Option<String> = None;
2018    let mut run_underline = false;
2019    let mut run_strike = false;
2020
2021    loop {
2022        match reader.read_event()? {
2023            Event::Eof => break,
2024
2025            Event::Start(start) if start.local_name().as_ref() == b"author" => {
2026                in_author = true;
2027                author_buffer.clear();
2028            }
2029            Event::End(end) if end.local_name().as_ref() == b"author" => {
2030                in_author = false;
2031                authors.push(std::mem::take(&mut author_buffer));
2032            }
2033            Event::Text(text) if in_author => {
2034                author_buffer.push_str(&xml_core::decode_text(&text)?);
2035            }
2036
2037            Event::Start(start) if start.local_name().as_ref() == b"comment" => {
2038                in_comment = true;
2039                current_ref = raw_attribute(&start, b"ref");
2040                current_author_id = raw_attribute(&start, b"authorId")
2041                    .and_then(|value| value.parse::<usize>().ok());
2042            }
2043            Event::End(end) if end.local_name().as_ref() == b"comment" => {
2044                in_comment = false;
2045                if let Some(cell) = current_ref.take() {
2046                    let author = current_author_id
2047                        .take()
2048                        .and_then(|id| authors.get(id).cloned())
2049                        .unwrap_or_default();
2050                    let comment = if text_is_rich {
2051                        CellComment::new(cell, author, String::new())
2052                            .with_rich_text(std::mem::take(&mut runs))
2053                    } else {
2054                        CellComment::new(cell, author, std::mem::take(&mut plain_buffer))
2055                    };
2056                    comments.push(comment);
2057                }
2058            }
2059
2060            Event::Start(start) if in_comment && start.local_name().as_ref() == b"text" => {
2061                in_comment_text = true;
2062                text_is_rich = false;
2063                plain_buffer.clear();
2064                runs.clear();
2065            }
2066            Event::End(end) if in_comment_text && end.local_name().as_ref() == b"text" => {
2067                in_comment_text = false;
2068            }
2069
2070            Event::Start(start) if in_comment_text && start.local_name().as_ref() == b"r" => {
2071                in_run = true;
2072                text_is_rich = true;
2073                run_text_buffer.clear();
2074                run_bold = false;
2075                run_italic = false;
2076                run_size = None;
2077                run_color = None;
2078                run_name = None;
2079                run_underline = false;
2080                run_strike = false;
2081            }
2082            Event::End(end) if in_comment_text && end.local_name().as_ref() == b"r" => {
2083                in_run = false;
2084                runs.push(RichTextRun {
2085                    text: std::mem::take(&mut run_text_buffer),
2086                    bold: std::mem::take(&mut run_bold),
2087                    italic: std::mem::take(&mut run_italic),
2088                    font_color: run_color.take(),
2089                    font_size: run_size.take(),
2090                    font_name: run_name.take(),
2091                    underline: std::mem::take(&mut run_underline),
2092                    strike: std::mem::take(&mut run_strike),
2093                });
2094            }
2095
2096            Event::Start(start) if in_run && start.local_name().as_ref() == b"rPr" => {
2097                in_rpr = true;
2098            }
2099            Event::End(end) if in_run && end.local_name().as_ref() == b"rPr" => {
2100                in_rpr = false;
2101            }
2102            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"b" => {
2103                run_bold = true;
2104            }
2105            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"i" => {
2106                run_italic = true;
2107            }
2108            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"strike" => {
2109                run_strike = true;
2110            }
2111            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"u" => {
2112                run_underline = true;
2113            }
2114            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"rFont" => {
2115                run_name = raw_attribute(&start, b"val");
2116            }
2117            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"sz" => {
2118                run_size =
2119                    raw_attribute(&start, b"val").and_then(|value| value.parse::<f64>().ok());
2120            }
2121            Event::Empty(start) if in_rpr && start.local_name().as_ref() == b"color" => {
2122                run_color = resolve_color(&start);
2123            }
2124
2125            Event::Start(start) if in_comment_text && start.local_name().as_ref() == b"t" => {
2126                in_text_element = true;
2127            }
2128            Event::End(end) if in_comment_text && end.local_name().as_ref() == b"t" => {
2129                in_text_element = false;
2130            }
2131            Event::Text(text) if in_text_element && in_run => {
2132                run_text_buffer.push_str(&xml_core::decode_text(&text)?);
2133            }
2134            Event::GeneralRef(reference) if in_text_element && in_run => {
2135                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
2136                    run_text_buffer.push_str(&resolved);
2137                }
2138            }
2139            Event::Text(text) if in_text_element && !in_run => {
2140                plain_buffer.push_str(&xml_core::decode_text(&text)?);
2141            }
2142            Event::GeneralRef(reference) if in_text_element && !in_run => {
2143                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
2144                    plain_buffer.push_str(&resolved);
2145                }
2146            }
2147
2148            _ => {}
2149        }
2150    }
2151
2152    Ok(comments)
2153}
2154
2155// ============================================================================,: sheet drawing
2156// (embedded pictures/charts).
2157// ============================================================================
2158
2159/// Parses one sheet's `xl/drawings/drawingN.xml` (`CT_Drawing`, root `<xdr:wsDr>`) back into a
2160/// [`SheetDrawing`] — the inverse of `writer.rs::to_drawing_xml`. `drawing_relationships` is this
2161/// drawing part's own `.rels` (the image/chart relationship ids embedded in the XML), and `package`
2162/// is the whole package, used to resolve those ids to the actual `xl/media/*`/`xl/charts/*` part
2163/// bytes. An anchor whose relationship or referenced part can't be resolved is silently skipped
2164/// (this crate's usual forgiving-reader posture) rather than erroring the whole sheet.
2165fn parse_drawing_xml(
2166    xml: &str,
2167    drawing_relationships: &opc::Relationships,
2168    package: &Package,
2169) -> Result<SheetDrawing> {
2170    let mut reader = Reader::from_xml_str(xml);
2171    let mut drawing = SheetDrawing::new();
2172
2173    loop {
2174        match reader.read_event()? {
2175            Event::Start(start) if start.local_name().as_ref() == b"twoCellAnchor" => {
2176                if let Some(anchor) =
2177                    parse_drawing_anchor(&mut reader, drawing_relationships, package)?
2178                {
2179                    drawing.anchors.push(anchor);
2180                }
2181            }
2182            Event::Eof => break,
2183            _ => {}
2184        }
2185    }
2186
2187    Ok(drawing)
2188}
2189
2190/// Parses one `<xdr:twoCellAnchor>`'s content (`from`/`to` markers, then exactly one of
2191/// `sp`/`grpSp`/`graphicFrame`/`cxnSp`/`pic`/`contentPart`, then `clientData`) from a reader
2192/// positioned right after the wrapper's own opening tag. Only `pic`/`graphicFrame` are recognized
2193/// (matching what this crate's own writer ever produces — see [`DrawingAnchor`]'s doc comment); any
2194/// other object kind is skipped and the whole anchor is dropped (returns `Ok(None)`), since
2195/// [`DrawingAnchor`] has no variant to hold it.
2196fn parse_drawing_anchor(
2197    reader: &mut Reader,
2198    drawing_relationships: &opc::Relationships,
2199    package: &Package,
2200) -> Result<Option<DrawingAnchor>> {
2201    let mut from: Option<CellAnchorPoint> = None;
2202    let mut to: Option<CellAnchorPoint> = None;
2203    let mut object: Option<DrawingObject> = None;
2204
2205    loop {
2206        match reader.read_event()? {
2207            Event::Start(start) if start.local_name().as_ref() == b"from" => {
2208                from = Some(parse_cell_anchor_point(reader)?);
2209            }
2210            Event::Start(start) if start.local_name().as_ref() == b"to" => {
2211                to = Some(parse_cell_anchor_point(reader)?);
2212            }
2213            Event::Start(start) if start.local_name().as_ref() == b"pic" => {
2214                object = parse_sheet_picture(reader, drawing_relationships, package)?
2215                    .map(|picture| DrawingObject::Picture(Box::new(picture)));
2216            }
2217            Event::Start(start) if start.local_name().as_ref() == b"graphicFrame" => {
2218                object = parse_sheet_chart(reader, drawing_relationships, package)?
2219                    .map(|chart| DrawingObject::Chart(Box::new(chart)));
2220            }
2221            Event::Start(_) => skip_subtree(reader)?,
2222            Event::Empty(start) if start.local_name().as_ref() == b"clientData" => {}
2223            Event::End(_) => break,
2224            Event::Eof => break,
2225            _ => {}
2226        }
2227    }
2228
2229    match (from, to, object) {
2230        (Some(from), Some(to), Some(object)) => Ok(Some(DrawingAnchor::new(from, to, object))),
2231        _ => Ok(None),
2232    }
2233}
2234
2235/// Parses one `<xdr:from>`/`<xdr:to>` marker (`CT_Marker`): `col`/`colOff`/ `row`/`rowOff`, from a
2236/// reader positioned right after the marker's own opening tag.
2237fn parse_cell_anchor_point(reader: &mut Reader) -> Result<CellAnchorPoint> {
2238    let mut point = CellAnchorPoint::default();
2239    let mut text_buffer = String::new();
2240    let mut current_field: Option<&str> = None;
2241
2242    loop {
2243        match reader.read_event()? {
2244            Event::Start(start) => {
2245                current_field = match start.local_name().as_ref() {
2246                    b"col" => Some("col"),
2247                    b"colOff" => Some("colOff"),
2248                    b"row" => Some("row"),
2249                    b"rowOff" => Some("rowOff"),
2250                    _ => None,
2251                };
2252                text_buffer.clear();
2253            }
2254            Event::Text(text) => {
2255                text_buffer.push_str(&xml_core::decode_text(&text)?);
2256            }
2257            Event::End(_) => {
2258                match current_field.take() {
2259                    Some("col") => point.column = text_buffer.trim().parse().unwrap_or(0),
2260                    Some("colOff") => {
2261                        point.column_offset_emu = text_buffer.trim().parse().unwrap_or(0)
2262                    }
2263                    Some("row") => point.row = text_buffer.trim().parse().unwrap_or(0),
2264                    Some("rowOff") => {
2265                        point.row_offset_emu = text_buffer.trim().parse().unwrap_or(0)
2266                    }
2267                    Some(_) | None => {
2268                        // The end of the marker's own wrapper (`from`/`to` itself) — nothing left
2269                        // inside it.
2270                        if current_field.is_none() && text_buffer.is_empty() {
2271                            return Ok(point);
2272                        }
2273                    }
2274                }
2275                text_buffer.clear();
2276            }
2277            Event::Eof => return Ok(point),
2278            _ => {}
2279        }
2280    }
2281}
2282
2283/// Parses one `<xdr:pic>` back into a [`SheetPicture`], from a reader positioned right after
2284/// `<xdr:pic>`'s own opening tag. Resolves the `<a:blip r:embed="..">` relationship id against
2285/// `drawing_relationships` to find the referenced `xl/media/*` part, and infers [`PictureFormat`]
2286/// from that part's file extension. Returns `Ok(None)` (dropping the whole anchor, see
2287/// [`parse_drawing_anchor`]) if the relationship or the media part can't be resolved, or the
2288/// extension isn't a supported format.
2289fn parse_sheet_picture(
2290    reader: &mut Reader,
2291    drawing_relationships: &opc::Relationships,
2292    package: &Package,
2293) -> Result<Option<SheetPicture>> {
2294    let mut name = String::new();
2295    let mut relationship_id: Option<String> = None;
2296    let mut shape_properties = None;
2297
2298    loop {
2299        match reader.read_event()? {
2300            // `nvPicPr` wraps `cNvPr`/`cNvPicPr` — recursed into directly (rather than via the
2301            // generic `Event::Start(_) => skip_subtree` catch-all below) since `cNvPr`'s `name`
2302            // attribute, one level deeper, is exactly what this function needs to extract.
2303            Event::Start(start) if start.local_name().as_ref() == b"nvPicPr" => {
2304                name = parse_non_visual_name(reader, b"nvPicPr")?;
2305            }
2306            Event::Start(start) if start.local_name().as_ref() == b"blipFill" => {
2307                relationship_id = parse_picture_blip_fill_relationship_id(reader)?;
2308            }
2309            Event::Empty(start) if start.local_name().as_ref() == b"blip" => {
2310                relationship_id = raw_attribute(&start, b"embed");
2311            }
2312            Event::Start(start) if start.local_name().as_ref() == b"spPr" => {
2313                // `drawing::read_shape_properties` already consumes `spPr`'s own closing tag as
2314                // part of its own loop termination (it breaks on the first unrecognized
2315                // `Event::End`, which is exactly that tag) — no trailing `skip_subtree` call here,
2316                // unlike the generic `Event::Start(_)` catch-all below. A redundant `skip_subtree`
2317                // call here previously ate `<xdr:pic>`'s own closing tag instead (the very next
2318                // event), which then cascaded into silently swallowing every subsequent sibling
2319                // anchor in the same drawing part — caught by
2320                // `round_trips_a_sheet_drawing_with_a_picture_and_a_chart_together` failing with
2321                // only 1 of 2 anchors surviving the round trip.
2322                shape_properties = Some(drawing::read_shape_properties(reader)?);
2323            }
2324            Event::Start(_) => skip_subtree(reader)?,
2325            Event::End(end) if end.local_name().as_ref() == b"pic" => break,
2326            Event::Eof => break,
2327            _ => {}
2328        }
2329    }
2330
2331    let Some(relationship_id) = relationship_id else {
2332        return Ok(None);
2333    };
2334    let Some(relationship) = drawing_relationships.by_id(&relationship_id) else {
2335        return Ok(None);
2336    };
2337    if relationship.rel_type != IMAGE_RELATIONSHIP_TYPE {
2338        return Ok(None);
2339    }
2340    let part_name = format!("/xl/{}", relationship.target.trim_start_matches("../"));
2341    let Some(part) = package.part(&part_name) else {
2342        return Ok(None);
2343    };
2344    let extension = part_name.rsplit('.').next().unwrap_or("");
2345    let Some(format) = PictureFormat::from_extension(extension) else {
2346        return Ok(None);
2347    };
2348
2349    let mut picture = SheetPicture::new(part.data.clone(), format).with_name(name);
2350    if let Some(mut shape_properties) = shape_properties {
2351        // `write_sheet_picture` now always writes its own fixed, zero-valued `<a:xfrm>` too
2352        // (matching real Excel-authored pictures, which always carry one — see that function's own
2353        // doc comment), never meaningful here since a picture's real position/size comes from its
2354        // `TwoCellAnchor`, not `spPr` — cleared back to `None` unconditionally, the same posture
2355        // `word_ooxml::parse_drawing` already applies to its own (differently-valued, but equally
2356        // non-authoritative) `<a:xfrm>`.
2357        shape_properties.transform = None;
2358
2359        // `write_sheet_picture` always writes *some* geometry (the picture's own, or a fixed
2360        // default `<a:prstGeom prst="rect">` when the caller didn't set one — see that function's
2361        // own doc comment for why: real Excel fails to render a picture at all without one). That
2362        // fixed default parses back as exactly `Geometry::Preset(PresetShape::Rectangle)` —
2363        // indistinguishable, on the wire, from a deliberately-set plain rectangle — so it's
2364        // normalized back to `None` here, the same "ambiguous fixed default treated as nothing set"
2365        // reasoning `word_ooxml::parse_drawing` already applies to its own picture geometry.
2366        if matches!(
2367            shape_properties.geometry,
2368            Some(drawing::Geometry::Preset(drawing::PresetShape::Rectangle))
2369        ) {
2370            shape_properties.geometry = None;
2371        }
2372        // `effects` must be included in this check: a picture whose only `shape_properties`
2373        // content is `effects` (no fill/line, geometry normalized away above) would otherwise be
2374        // wrongly treated as "nothing set at all" and silently discarded here, even though
2375        // `effects` was just correctly parsed two lines above.
2376        let is_fixed_default = shape_properties.geometry.is_none()
2377            && shape_properties.fill.is_none()
2378            && shape_properties.line.is_none()
2379            && shape_properties.effects.is_none();
2380        if !is_fixed_default {
2381            picture = picture.with_shape_properties(shape_properties);
2382        }
2383    }
2384    Ok(Some(picture))
2385}
2386
2387/// Parses `<xdr:blipFill>`'s content (looking only for `<a:blip r:embed="..">`) from a reader
2388/// positioned right after `<xdr:blipFill>`'s own opening tag, consuming through to its closing tag.
2389fn parse_picture_blip_fill_relationship_id(reader: &mut Reader) -> Result<Option<String>> {
2390    let mut relationship_id = None;
2391    loop {
2392        match reader.read_event()? {
2393            Event::Empty(start) if start.local_name().as_ref() == b"blip" => {
2394                relationship_id = raw_attribute(&start, b"embed");
2395            }
2396            Event::Start(start) if start.local_name().as_ref() == b"blip" => {
2397                relationship_id = raw_attribute(&start, b"embed");
2398                skip_subtree(reader)?;
2399            }
2400            Event::Start(_) => skip_subtree(reader)?,
2401            Event::End(end) if end.local_name().as_ref() == b"blipFill" => break,
2402            Event::Eof => break,
2403            _ => {}
2404        }
2405    }
2406    Ok(relationship_id)
2407}
2408
2409/// Parses one `<xdr:graphicFrame>` back into a [`SheetChart`], from a reader positioned right after
2410/// `<xdr:graphicFrame>`'s own opening tag. Resolves the `<c:chart r:id="..">` relationship id
2411/// against `drawing_relationships` to find the referenced `xl/charts/*` part, then parses its whole
2412/// content via `chart::read_chart_space`. Returns `Ok(None)` (dropping the whole anchor) if the
2413/// relationship or the chart part can't be resolved.
2414fn parse_sheet_chart(
2415    reader: &mut Reader,
2416    drawing_relationships: &opc::Relationships,
2417    package: &Package,
2418) -> Result<Option<SheetChart>> {
2419    let mut name = String::new();
2420    let mut relationship_id: Option<String> = None;
2421
2422    loop {
2423        match reader.read_event()? {
2424            // `nvGraphicFramePr` wraps `cNvPr`/`cNvGraphicFramePr` — same reasoning as
2425            // `parse_sheet_picture`'s `nvPicPr` handling above.
2426            Event::Start(start) if start.local_name().as_ref() == b"nvGraphicFramePr" => {
2427                name = parse_non_visual_name(reader, b"nvGraphicFramePr")?;
2428            }
2429            Event::Empty(start) if start.local_name().as_ref() == b"chart" => {
2430                relationship_id = raw_attribute(&start, b"id");
2431            }
2432            // `<a:graphic><a:graphicData>` are pure pass-through wrappers around `<c:chart
2433            // r:id="..">` — descended into transparently (not skipped) so the loop reaches that
2434            // nested element, unlike `xfrm`/any other unrecognized child, which genuinely should be
2435            // skipped whole.
2436            Event::Start(start)
2437                if matches!(start.local_name().as_ref(), b"graphic" | b"graphicData") => {}
2438            Event::Start(_) => skip_subtree(reader)?,
2439            Event::End(end) if end.local_name().as_ref() == b"graphicFrame" => break,
2440            Event::Eof => break,
2441            _ => {}
2442        }
2443    }
2444
2445    let Some(relationship_id) = relationship_id else {
2446        return Ok(None);
2447    };
2448    let Some(relationship) = drawing_relationships.by_id(&relationship_id) else {
2449        return Ok(None);
2450    };
2451    if relationship.rel_type != CHART_RELATIONSHIP_TYPE {
2452        return Ok(None);
2453    }
2454    let part_name = format!("/xl/{}", relationship.target.trim_start_matches("../"));
2455    let Some(part) = package.part(&part_name) else {
2456        return Ok(None);
2457    };
2458    let xml = std::str::from_utf8(&part.data)
2459        .map_err(|error| Error::InvalidUtf8(part_name.clone(), error))?;
2460    let chart_space = chart::read_chart_space(xml)?;
2461
2462    Ok(Some(SheetChart::new(chart_space).with_name(name)))
2463}
2464
2465/// Parses a `<xdr:nvPicPr>`/`<xdr:nvGraphicFramePr>` non-visual-properties wrapper's own
2466/// `cNvPr/@name` attribute, from a reader positioned right after the wrapper's own opening tag;
2467/// stops right after consuming the wrapper's closing tag (identified by `wrapper_local_name`). The
2468/// sibling `cNvPicPr`/`cNvGraphicFramePr` child (no attributes this crate reads back) is skipped
2469/// whole.
2470fn parse_non_visual_name(reader: &mut Reader, wrapper_local_name: &[u8]) -> Result<String> {
2471    let mut name = String::new();
2472    loop {
2473        match reader.read_event()? {
2474            Event::Empty(start) if start.local_name().as_ref() == b"cNvPr" => {
2475                name = raw_attribute(&start, b"name").unwrap_or_default();
2476            }
2477            Event::Start(start) if start.local_name().as_ref() == b"cNvPr" => {
2478                name = raw_attribute(&start, b"name").unwrap_or_default();
2479                skip_subtree(reader)?;
2480            }
2481            Event::Start(_) => skip_subtree(reader)?,
2482            Event::End(end) if end.local_name().as_ref() == wrapper_local_name => break,
2483            Event::Eof => break,
2484            _ => {}
2485        }
2486    }
2487    Ok(name)
2488}
2489
2490/// Skips an element's whole subtree (depth-tracked), stopping right after consuming its own closing
2491/// tag — used to tolerantly skip over unrecognized or not-yet-modeled children while parsing a
2492/// drawing part, same posture as `drawing`'s own private helper of the same name.
2493fn skip_subtree(reader: &mut Reader) -> Result<()> {
2494    let mut depth: u32 = 0;
2495    loop {
2496        match reader.read_event()? {
2497            Event::Start(_) => depth += 1,
2498            Event::End(_) => {
2499                if depth == 0 {
2500                    return Ok(());
2501                }
2502                depth -= 1;
2503            }
2504            Event::Eof => return Ok(()),
2505            _ => {}
2506        }
2507    }
2508}
2509
2510/// Parses `xl/tables/tableN.xml` (`CT_Table`) into an [`ExcelTable`] —
2511/// (`name`/`ref`/`totalsRowShown` and each `<tableColumn>`'s plain `name`), extended for
2512/// `calculatedColumnFormula`/ `totalsRowFunction`/`totalsRowLabel`. A built-in
2513/// `totalsRowFunction`'s own `<totalsRowFormula>` child (this crate's own writer always
2514/// auto-generates it, see `writer.rs::write_table_column`'s doc comment) is intentionally *not*
2515/// read back into `TableColumn::totals_row_formula` — only `Custom`'s formula text is, since every
2516/// other function is re-derived from `totals_row_function` alone on the next write. A real
2517/// third-party file's built-in-function formula that happens to differ from this crate's own
2518/// auto-generated `SUBTOTAL` text is therefore not preserved verbatim on a round trip — an
2519/// accepted, minor scope limit.
2520fn parse_table_xml(xml: &str) -> Result<ExcelTable> {
2521    let mut reader = Reader::from_xml_str(xml);
2522    let mut name = String::new();
2523    let mut range = String::new();
2524    let mut show_totals_row = false;
2525    let mut columns: Vec<TableColumn> = Vec::new();
2526
2527    let mut current_column: Option<TableColumn> = None;
2528    let mut in_calculated_column_formula = false;
2529    let mut in_totals_row_formula = false;
2530    let mut formula_buffer = String::new();
2531
2532    // `<sortState>`/`<sortCondition>`. A direct sibling of `<autoFilter>` under `<table>`
2533    // (`CT_Table`'s own sequence, confirmed via `sml.xsd`), not nested inside it — see
2534    // `ExcelTable.sort_state`'s doc comment.
2535    let mut sort_state: Option<SortState> = None;
2536    let mut current_sort_state: Option<SortState> = None;
2537
2538    // `<tableStyleInfo name="..">`.
2539    let mut table_style_name: Option<String> = None;
2540
2541    loop {
2542        match reader.read_event()? {
2543            Event::Eof => break,
2544
2545            Event::Start(start) | Event::Empty(start)
2546                if start.local_name().as_ref() == b"table" =>
2547            {
2548                name = raw_attribute(&start, b"name").unwrap_or_default();
2549                range = raw_attribute(&start, b"ref").unwrap_or_default();
2550                // `totalsRowCount` (default `"0"`) is what actually reserves a totals row at all —
2551                // `totalsRowShown` (default `"true"`) only controls whether an already-reserved one
2552                // is displayed. This crate's own writer always pairs `totalsRowCount="1"` with
2553                // `totalsRowShown="1"` (see `writer.rs::to_table_xml`'s own doc comment on this
2554                // attribute pair), but an arbitrary real file may rely on `totalsRowShown`'s
2555                // default (omitted, meaning `"true"`) while still setting `totalsRowCount`
2556                // explicitly.
2557                let totals_row_count = raw_attribute(&start, b"totalsRowCount")
2558                    .and_then(|value| value.parse::<u32>().ok())
2559                    .unwrap_or(0);
2560                let totals_row_shown = raw_attribute(&start, b"totalsRowShown")
2561                    .as_deref()
2562                    .map(|value| value != "0")
2563                    .unwrap_or(true);
2564                show_totals_row = totals_row_count > 0 && totals_row_shown;
2565            }
2566
2567            Event::Empty(start) if start.local_name().as_ref() == b"tableColumn" => {
2568                let mut column =
2569                    TableColumn::new(raw_attribute(&start, b"name").unwrap_or_default());
2570                column.totals_row_function =
2571                    parse_totals_row_function(raw_attribute(&start, b"totalsRowFunction"));
2572                column.totals_row_label = raw_attribute(&start, b"totalsRowLabel");
2573                columns.push(column);
2574            }
2575            Event::Start(start) if start.local_name().as_ref() == b"tableColumn" => {
2576                let mut column =
2577                    TableColumn::new(raw_attribute(&start, b"name").unwrap_or_default());
2578                column.totals_row_function =
2579                    parse_totals_row_function(raw_attribute(&start, b"totalsRowFunction"));
2580                column.totals_row_label = raw_attribute(&start, b"totalsRowLabel");
2581                current_column = Some(column);
2582            }
2583            Event::End(end) if end.local_name().as_ref() == b"tableColumn" => {
2584                if let Some(column) = current_column.take() {
2585                    columns.push(column);
2586                }
2587            }
2588
2589            Event::Start(start)
2590                if current_column.is_some()
2591                    && start.local_name().as_ref() == b"calculatedColumnFormula" =>
2592            {
2593                in_calculated_column_formula = true;
2594                formula_buffer.clear();
2595            }
2596            Event::End(end)
2597                if in_calculated_column_formula
2598                    && end.local_name().as_ref() == b"calculatedColumnFormula" =>
2599            {
2600                in_calculated_column_formula = false;
2601                if let Some(column) = current_column.as_mut() {
2602                    column.calculated_formula = Some(std::mem::take(&mut formula_buffer));
2603                }
2604            }
2605            Event::Start(start)
2606                if current_column.is_some()
2607                    && start.local_name().as_ref() == b"totalsRowFormula" =>
2608            {
2609                in_totals_row_formula = true;
2610                formula_buffer.clear();
2611            }
2612            Event::End(end)
2613                if in_totals_row_formula && end.local_name().as_ref() == b"totalsRowFormula" =>
2614            {
2615                in_totals_row_formula = false;
2616                if let Some(column) = current_column.as_mut() {
2617                    // Only `Custom` needs its formula text preserved — see this function's own doc
2618                    // comment for why built-in functions' `<totalsRowFormula>` is intentionally not
2619                    // read back.
2620                    if column.totals_row_function == Some(TotalsRowFunction::Custom) {
2621                        column.totals_row_formula = Some(std::mem::take(&mut formula_buffer));
2622                    } else {
2623                        formula_buffer.clear();
2624                    }
2625                }
2626            }
2627            Event::Text(text) if in_calculated_column_formula || in_totals_row_formula => {
2628                formula_buffer.push_str(&xml_core::decode_text(&text)?);
2629            }
2630            Event::GeneralRef(reference)
2631                if in_calculated_column_formula || in_totals_row_formula =>
2632            {
2633                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
2634                    formula_buffer.push_str(&resolved);
2635                }
2636            }
2637
2638            // `<sortState ref="..">`/`<sortCondition ref=".." descending="..">`.
2639            Event::Start(start) if start.local_name().as_ref() == b"sortState" => {
2640                current_sort_state = Some(SortState::new(
2641                    raw_attribute(&start, b"ref").unwrap_or_default(),
2642                ));
2643            }
2644            Event::Empty(start) if start.local_name().as_ref() == b"sortState" => {
2645                sort_state = Some(SortState::new(
2646                    raw_attribute(&start, b"ref").unwrap_or_default(),
2647                ));
2648            }
2649            Event::End(end) if end.local_name().as_ref() == b"sortState" => {
2650                sort_state = current_sort_state.take();
2651            }
2652            Event::Empty(start)
2653                if current_sort_state.is_some()
2654                    && start.local_name().as_ref() == b"sortCondition" =>
2655            {
2656                if let Some(range) = raw_attribute(&start, b"ref") {
2657                    let descending = raw_attribute(&start, b"descending").as_deref() == Some("1");
2658                    if let Some(state) = current_sort_state.as_mut() {
2659                        let mut condition = SortCondition::new(range);
2660                        condition.descending = descending;
2661                        state.conditions.push(condition);
2662                    }
2663                }
2664            }
2665
2666            // `<tableStyleInfo name="..">`.
2667            Event::Start(start) | Event::Empty(start)
2668                if start.local_name().as_ref() == b"tableStyleInfo" =>
2669            {
2670                table_style_name = raw_attribute(&start, b"name");
2671            }
2672
2673            _ => {}
2674        }
2675    }
2676
2677    Ok(ExcelTable {
2678        name,
2679        range,
2680        columns,
2681        show_totals_row,
2682        sort_state,
2683        table_style_name,
2684    })
2685}
2686
2687/// Maps `CT_TableColumn`'s `totalsRowFunction` attribute value back to a [`TotalsRowFunction`] —
2688/// the reader-side counterpart of `writer.rs::totals_row_function_str`. An absent or unrecognized
2689/// attribute value resolves to `None` (Excel's own default: no totals row function on that column).
2690fn parse_totals_row_function(value: Option<String>) -> Option<TotalsRowFunction> {
2691    match value.as_deref() {
2692        Some("sum") => Some(TotalsRowFunction::Sum),
2693        Some("average") => Some(TotalsRowFunction::Average),
2694        Some("count") => Some(TotalsRowFunction::Count),
2695        Some("countNums") => Some(TotalsRowFunction::CountNums),
2696        Some("max") => Some(TotalsRowFunction::Max),
2697        Some("min") => Some(TotalsRowFunction::Min),
2698        Some("stdDev") => Some(TotalsRowFunction::StdDev),
2699        Some("var") => Some(TotalsRowFunction::Var),
2700        Some("custom") => Some(TotalsRowFunction::Custom),
2701        _ => None,
2702    }
2703}
2704
2705/// Everything [`parse_worksheet_xml`] extracts from a single `xl/worksheets/sheetN.xml` — grouped
2706/// into one struct rather than a (by now unwieldy) growing tuple, see this function's own doc
2707/// comment for what each field covers.
2708struct ParsedWorksheet {
2709    rows: Vec<Row>,
2710    conditional_formatting_rules: Vec<ConditionalFormattingRule>,
2711    data_validation_rules: Vec<DataValidationRule>,
2712    merged_ranges: Vec<String>,
2713    column_settings: Vec<ColumnSetting>,
2714    auto_filter_range: Option<String>,
2715    freeze_panes: Option<FreezePane>,
2716    protection: Option<SheetProtection>,
2717    hyperlinks: Vec<CellHyperlink>,
2718    zoom_scale: Option<u32>,
2719    row_breaks: Vec<u32>,
2720    column_breaks: Vec<u32>,
2721    scenarios: Vec<Scenario>,
2722    auto_filter_columns: Vec<AutoFilterColumn>,
2723    /// 's read side (never implemented until now) plus (first/even header/footer).
2724    print_settings: PrintSettings,
2725    /// (`<sheetPr><tabColor>`).
2726    tab_color: Option<String>,
2727    show_grid_lines: Option<bool>,
2728    show_row_col_headers: Option<bool>,
2729    show_zeros: Option<bool>,
2730    right_to_left: Option<bool>,
2731    /// (`<selection activeCell>`).
2732    active_cell: Option<String>,
2733    /// (`<sheetFormatPr>`).
2734    default_column_width: Option<f64>,
2735    /// (`<sheetFormatPr>`).
2736    default_row_height: Option<f64>,
2737    /// The point 48 (`<ignoredErrors>`/`<ignoredError>`).
2738    ignored_errors: Vec<IgnoredError>,
2739    /// The point 51 (`<protectedRanges>`/`<protectedRange>`).
2740    protected_ranges: Vec<ProtectedRange>,
2741    /// The point 53 (`<autoFilter><sortState>`).
2742    sort_state: Option<SortState>,
2743    /// The point 55 (`<sheetPr><outlinePr summaryBelow>`).
2744    outline_summary_below: Option<bool>,
2745    /// The point 55 (`<sheetPr><outlinePr summaryRight>`).
2746    outline_summary_right: Option<bool>,
2747}
2748
2749/// Parses one sheet's `<sheetData>` into a dense `Vec<Row>` — see `Sheet.rows`/`Row.cells`'s own
2750/// doc comments for why a row/cell's position in the resulting `Vec`s is reconstructed from `<row
2751/// r=".">`/ `<c r=".">` rather than assumed to be gap-free (real Excel omits genuinely empty
2752/// rows/cells from the XML entirely). Also parses `<cols>`, `<sheetProtection>`, `<autoFilter>`,
2753/// `<mergeCells>`, `<conditionalFormatting>`/`<cfRule>`, `<dataValidations>`/ `<dataValidation>`,
2754/// `<hyperlinks>`, and a frozen `<pane>` inside `<sheetViews>` — into [`ParsedWorksheet`]'s other
2755/// fields.
2756///
2757/// A cell's `s` attribute (its `cellXfs` index) is resolved against `cell_formats` (see
2758/// `parse_styles_xml`) into `Cell.format`. A `<f>` child makes the cell a `CellValue::Formula` —
2759/// `CellValue::ArrayFormula` for `t="array"`, `CellValue::SharedFormula` for `t="shared"` (point
2760/// 52,) — a self-closing `<f../>` (a shared-formula follower cell with no literal text) still
2761/// counts as "has a formula", just with `formula: None`. `t="b"` (boolean) and `t="s"` resolving to
2762/// a `SharedStringEntry::Rich` entry are both modeled now; a cell whose `t` is `"str"`/`"e"`/
2763/// `"inlineStr"` and which has no `<f>` either is still not modeled and reads back as
2764/// `CellValue::Empty`. A date-formatted numeric cell is never reconstructed into `CellValue::Date`
2765/// (see that variant's own doc comment) — it simply reads back as `CellValue::Number`.
2766///
2767/// A `cfRule@dxfId` is resolved against `dxfs` (see `parse_styles_xml`'s doc comment); an
2768/// unrecognized `cfRule@type`/`dataValidation@type`/ `@operator` silently drops that one rule
2769/// rather than failing the whole parse — matching this reader's existing forgiving posture for
2770/// content it doesn't model. `sheetProtection` always resolves to `SheetProtection::locked()`
2771/// (`password: None`) regardless of whether a `password` attribute was present — the legacy hash is
2772/// one-way, this crate cannot and does not try to recover the original password, see
2773/// `SheetProtection.password`'s doc comment. `sheet_relationships` (this worksheet's own `.rels`)
2774/// is needed to resolve an external hyperlink's `r:id` back to its URL.
2775fn parse_worksheet_xml(
2776    xml: &str,
2777    shared_strings: &[SharedStringEntry],
2778    cell_formats: &[Option<CellFormat>],
2779    dxfs: &[CellFormat],
2780    sheet_relationships: &opc::Relationships,
2781) -> Result<ParsedWorksheet> {
2782    let mut reader = Reader::from_xml_str(xml);
2783    let mut rows: Vec<Row> = Vec::new();
2784
2785    let mut in_sheet_data = false;
2786    let mut current_row_cells: Vec<Cell> = Vec::new();
2787    let mut current_row_number: Option<usize> = None;
2788    let mut current_row_height: Option<f64> = None;
2789    let mut current_row_hidden = false;
2790    let mut current_row_outline_level: u8 = 0;
2791    // `<row collapsed="1">`.
2792    let mut current_row_collapsed = false;
2793    // `<row s=".." customFormat="1">`. Resolved through `cell_formats` exactly like a cell's own
2794    // `s` attribute (see `current_cell_style`'s resolution below).
2795    let mut current_row_style: Option<usize> = None;
2796
2797    let mut current_cell_column: Option<usize> = None;
2798    let mut current_cell_type: Option<String> = None;
2799    let mut current_cell_style: Option<usize> = None;
2800    let mut in_value = false;
2801    let mut value_buffer = String::new();
2802    let mut in_formula = false;
2803    let mut has_formula = false;
2804    let mut formula_buffer = String::new();
2805    // `<f t="array" ref="..">`. Only the array range's anchor (top-left) cell carries this
2806    // `t`/`ref` pair in a real Excel file (see this crate's own `writer.rs::write_cell`'s
2807    // `CellValue::ArrayFormula` arm) — every other cell in the range has no `<f>` at all and reads
2808    // back as a plain numeric `CellValue::Number` from its own `<v>`, same as before this point
2809    // existed.
2810    let mut current_formula_type: Option<String> = None;
2811    let mut current_formula_ref: Option<String> = None;
2812    // `<f t="shared" si="..">`.
2813    let mut current_formula_si: Option<u32> = None;
2814
2815    // `<cols>`/`<col>` parsing state.
2816    let mut column_settings: Vec<ColumnSetting> = Vec::new();
2817
2818    // `<mergeCells>`/`<mergeCell>` — point 2.
2819    let mut merged_ranges: Vec<String> = Vec::new();
2820
2821    // `<autoFilter>` — point 6.
2822    let mut auto_filter_range: Option<String> = None;
2823
2824    // `<sheetProtection>` — point 13.
2825    let mut protection: Option<SheetProtection> = None;
2826
2827    // `<sheetViews><sheetView><pane../></sheetView></sheetViews>` — point 9. Only a
2828    // `state="frozen"` pane is modeled, matching the writer (see `FreezePane`'s doc comment).
2829    let mut freeze_panes: Option<FreezePane> = None;
2830
2831    // `<hyperlinks>`/`<hyperlink>` — point 10.
2832    let mut hyperlinks: Vec<CellHyperlink> = Vec::new();
2833
2834    // `<sheetView zoomScale="..">` — point 29.
2835    let mut zoom_scale: Option<u32> = None;
2836
2837    // `<sheetView showGridLines showRowColHeaders showZeros rightToLeft>`.
2838    let mut show_grid_lines: Option<bool> = None;
2839    let mut show_row_col_headers: Option<bool> = None;
2840    let mut show_zeros: Option<bool> = None;
2841    let mut right_to_left: Option<bool> = None;
2842
2843    // `<selection activeCell="..">` — point 40.
2844    let mut active_cell: Option<String> = None;
2845
2846    // `<sheetPr><tabColor rgb="..">` — point 38.
2847    let mut tab_color: Option<String> = None;
2848
2849    // `<sheetPr><outlinePr summaryBelow summaryRight>`.
2850    let mut outline_summary_below: Option<bool> = None;
2851    let mut outline_summary_right: Option<bool> = None;
2852
2853    // `<sheetFormatPr defaultColWidth defaultRowHeight>` — point 41.
2854    let mut default_column_width: Option<f64> = None;
2855    let mut default_row_height: Option<f64> = None;
2856
2857    // `<ignoredErrors>`/`<ignoredError>`.
2858    let mut ignored_errors: Vec<IgnoredError> = Vec::new();
2859
2860    // `<protectedRanges>`/`<protectedRange>`. Like `SheetProtection`'s own read side, a range's
2861    // password is never recovered (one-way legacy hash) — every entry reads back with `password:
2862    // None`.
2863    let mut protected_ranges: Vec<ProtectedRange> = Vec::new();
2864
2865    // `<rowBreaks>`/`<colBreaks>`/`<brk>` — point 30. Both elements share the same `<brk>` child
2866    // shape, disambiguated by which container is currently open.
2867    let mut row_breaks: Vec<u32> = Vec::new();
2868    let mut column_breaks: Vec<u32> = Vec::new();
2869    let mut in_row_breaks = false;
2870    let mut in_col_breaks = false;
2871
2872    // `<scenarios>`/`<scenario>`/`<inputCells>` — point 35.
2873    let mut scenarios: Vec<Scenario> = Vec::new();
2874    let mut current_scenario: Option<Scenario> = None;
2875
2876    // `<autoFilter>`/`<filterColumn>`/`<filters>`/`<filter>` — point 36.
2877    let mut auto_filter_columns: Vec<AutoFilterColumn> = Vec::new();
2878    let mut current_filter_column: Option<AutoFilterColumn> = None;
2879
2880    // `<autoFilter><sortState>..</sortState></autoFilter>`. Nested inside `<autoFilter>` here,
2881    // unlike a table's own `<sortState>` (a sibling, parsed separately by `parse_table_xml`) — see
2882    // `Sheet.sort_state`'s doc comment.
2883    let mut sort_state: Option<SortState> = None;
2884    let mut current_sort_state: Option<SortState> = None;
2885
2886    // `<pageSetup>`/`<headerFooter>` — point 12's read side (new) plus point 31 (first/even
2887    // variants).
2888    let mut orientation: Option<PageOrientation> = None;
2889    let mut fit_to_width: Option<u32> = None;
2890    let mut fit_to_height: Option<u32> = None;
2891    // `<pageSetup scale="..">` — point 47.
2892    let mut scale: Option<u32> = None;
2893    let mut header: Option<String> = None;
2894    let mut footer: Option<String> = None;
2895    let mut header_first: Option<String> = None;
2896    let mut footer_first: Option<String> = None;
2897    let mut header_even: Option<String> = None;
2898    let mut footer_even: Option<String> = None;
2899    let mut in_header_footer_element: Option<&'static str> = None;
2900    let mut header_footer_buffer = String::new();
2901
2902    // `<conditionalFormatting>`/`<cfRule>` parsing state.
2903    let mut conditional_formatting_rules: Vec<ConditionalFormattingRule> = Vec::new();
2904    let mut current_cf_sqref: Option<String> = None;
2905    let mut in_cf_rule = false;
2906    let mut cf_rule_type: Option<String> = None;
2907    let mut cf_rule_dxf_id: Option<usize> = None;
2908    let mut cf_rule_operator: Option<String> = None;
2909    let mut cf_rule_text: Option<String> = None;
2910    let mut cf_formulas: Vec<String> = Vec::new();
2911    let mut in_cf_formula = false;
2912    let mut cf_formula_buffer = String::new();
2913
2914    // `<dataValidations>`/`<dataValidation>` parsing state.
2915    let mut data_validation_rules: Vec<DataValidationRule> = Vec::new();
2916    let mut in_data_validation = false;
2917    let mut dv_type: Option<String> = None;
2918    let mut dv_operator: Option<String> = None;
2919    let mut dv_allow_blank = false;
2920    let mut dv_show_input_message = false;
2921    let mut dv_show_error_message = false;
2922    let mut dv_prompt_title: Option<String> = None;
2923    let mut dv_prompt: Option<String> = None;
2924    let mut dv_error_title: Option<String> = None;
2925    let mut dv_error: Option<String> = None;
2926    let mut dv_sqref: Option<String> = None;
2927    let mut dv_formula1: Option<String> = None;
2928    let mut dv_formula2: Option<String> = None;
2929    let mut in_dv_formula1 = false;
2930    let mut in_dv_formula2 = false;
2931    let mut dv_formula_buffer = String::new();
2932
2933    loop {
2934        match reader.read_event()? {
2935            Event::Eof => break,
2936
2937            Event::Start(start) if start.local_name().as_ref() == b"sheetData" => {
2938                in_sheet_data = true;
2939            }
2940            Event::End(end) if end.local_name().as_ref() == b"sheetData" => {
2941                in_sheet_data = false;
2942            }
2943
2944            Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"row" => {
2945                current_row_number =
2946                    raw_attribute(&start, b"r").and_then(|value| value.parse::<usize>().ok());
2947                current_row_cells = Vec::new();
2948                current_row_height =
2949                    raw_attribute(&start, b"ht").and_then(|value| value.parse::<f64>().ok());
2950                current_row_hidden = raw_attribute(&start, b"hidden").as_deref() == Some("1");
2951                current_row_outline_level = raw_attribute(&start, b"outlineLevel")
2952                    .and_then(|value| value.parse::<u8>().ok())
2953                    .unwrap_or(0);
2954                current_row_collapsed = raw_attribute(&start, b"collapsed").as_deref() == Some("1");
2955                // Only resolved into `default_format` when `customFormat="1"` is also present,
2956                // matching the writer's always-paired convention (see `current_row_style`'s doc
2957                // comment above).
2958                current_row_style =
2959                    if raw_attribute(&start, b"customFormat").as_deref() == Some("1") {
2960                        raw_attribute(&start, b"s").and_then(|value| value.parse::<usize>().ok())
2961                    } else {
2962                        None
2963                    };
2964            }
2965            Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"row" => {
2966                // `.max(1)`: `ST_RowIndex` is always >= 1 in a well-formed file, but guards against
2967                // a malformed `r="0"` underflowing the `- 1` below.
2968                let row_number = current_row_number.take().unwrap_or(rows.len() + 1).max(1);
2969                let default_format = current_row_style
2970                    .take()
2971                    .and_then(|index| cell_formats.get(index).cloned().flatten());
2972                let cells = std::mem::take(&mut current_row_cells);
2973                let height = current_row_height.take();
2974                let hidden = std::mem::take(&mut current_row_hidden);
2975                let outline_level = std::mem::take(&mut current_row_outline_level);
2976                let collapsed = std::mem::take(&mut current_row_collapsed);
2977                // A `row_number` beyond `EXCEL_MAX_ROWS` can only come from a malformed/adversarial
2978                // `r=".."` attribute — a real `.xlsx` never has one. Drop the row instead of
2979                // trusting it to size `rows`: without this, a file a few hundred bytes long
2980                // claiming `r="9999999999"` would make `ensure_len` try to allocate billions of
2981                // `Row`s.
2982                if row_number <= EXCEL_MAX_ROWS {
2983                    ensure_len(&mut rows, row_number, Row::default);
2984                    rows[row_number - 1] = Row {
2985                        cells,
2986                        height,
2987                        hidden,
2988                        outline_level,
2989                        collapsed,
2990                        default_format,
2991                    };
2992                }
2993            }
2994
2995            Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"c" => {
2996                current_cell_type = raw_attribute(&start, b"t");
2997                current_cell_column = raw_attribute(&start, b"r")
2998                    .as_deref()
2999                    .and_then(column_index_from_reference);
3000                current_cell_style =
3001                    raw_attribute(&start, b"s").and_then(|value| value.parse::<usize>().ok());
3002                has_formula = false;
3003                formula_buffer.clear();
3004            }
3005            Event::Empty(start) if in_sheet_data && start.local_name().as_ref() == b"c" => {
3006                // A self-closing `<c r=".."/>` with no `<v>` at all (e.g. a styled-but-empty cell)
3007                // — still reserve its column position so later cells in the row stay aligned, and
3008                // resolve its style if it carries one.
3009                if let Some(column_index) = raw_attribute(&start, b"r")
3010                    .as_deref()
3011                    .and_then(column_index_from_reference)
3012                {
3013                    ensure_len(&mut current_row_cells, column_index + 1, Cell::default);
3014                    let format = raw_attribute(&start, b"s")
3015                        .and_then(|value| value.parse::<usize>().ok())
3016                        .and_then(|index| cell_formats.get(index).cloned().flatten());
3017                    current_row_cells[column_index] = Cell {
3018                        value: CellValue::Empty,
3019                        format,
3020                    };
3021                }
3022            }
3023            Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"c" => {
3024                if let Some(column_index) = current_cell_column.take() {
3025                    ensure_len(&mut current_row_cells, column_index + 1, Cell::default);
3026                    let raw_value = std::mem::take(&mut value_buffer);
3027                    let cell_type = current_cell_type.take();
3028                    let format = current_cell_style
3029                        .take()
3030                        .and_then(|index| cell_formats.get(index).cloned().flatten());
3031                    let formula_text = std::mem::take(&mut formula_buffer);
3032                    let had_formula = std::mem::take(&mut has_formula);
3033                    let formula_type = current_formula_type.take();
3034                    let formula_ref = current_formula_ref.take();
3035                    let formula_si = current_formula_si.take();
3036
3037                    let value = if had_formula && formula_type.as_deref() == Some("array") {
3038                        CellValue::ArrayFormula {
3039                            formula: formula_text,
3040                            range: formula_ref.unwrap_or_default(),
3041                            cached_value: raw_value.parse::<f64>().ok(),
3042                        }
3043                    } else if had_formula && formula_type.as_deref() == Some("shared") {
3044                        // `si` is required on a real `t="shared"` cell per `sml.xsd`, but this
3045                        // crate degrades gracefully to group `0` if a malformed file omits it
3046                        // rather than dropping the cell entirely.
3047                        CellValue::SharedFormula {
3048                            formula: if formula_text.is_empty() {
3049                                None
3050                            } else {
3051                                Some(formula_text)
3052                            },
3053                            group_index: formula_si.unwrap_or(0),
3054                            range: formula_ref,
3055                            cached_value: raw_value.parse::<f64>().ok(),
3056                        }
3057                    } else if had_formula {
3058                        CellValue::Formula {
3059                            formula: if formula_text.is_empty() {
3060                                None
3061                            } else {
3062                                Some(formula_text)
3063                            },
3064                            cached_value: raw_value.parse::<f64>().ok(),
3065                        }
3066                    } else {
3067                        match cell_type.as_deref() {
3068                            Some("s") => raw_value
3069                                .parse::<usize>()
3070                                .ok()
3071                                .and_then(|index| shared_strings.get(index))
3072                                .map(|entry| match entry {
3073                                    SharedStringEntry::Plain(text) => CellValue::Text(text.clone()),
3074                                    SharedStringEntry::Rich(runs) => {
3075                                        CellValue::RichText(runs.clone())
3076                                    }
3077                                })
3078                                .unwrap_or(CellValue::Empty),
3079                            // A boolean cell (`t="b"`).
3080                            Some("b") => CellValue::Boolean(raw_value == "1"),
3081                            None | Some("n") => raw_value
3082                                .parse::<f64>()
3083                                .ok()
3084                                .map(CellValue::Number)
3085                                .unwrap_or(CellValue::Empty),
3086                            _ => CellValue::Empty,
3087                        }
3088                    };
3089                    current_row_cells[column_index] = Cell { value, format };
3090                }
3091            }
3092
3093            Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"v" => {
3094                in_value = true;
3095                value_buffer.clear();
3096            }
3097            Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"v" => {
3098                in_value = false;
3099            }
3100            Event::Text(text) if in_value => {
3101                value_buffer.push_str(&xml_core::decode_text(&text)?);
3102            }
3103
3104            Event::Start(start) if in_sheet_data && start.local_name().as_ref() == b"f" => {
3105                in_formula = true;
3106                has_formula = true;
3107                current_formula_type = raw_attribute(&start, b"t");
3108                current_formula_ref = raw_attribute(&start, b"ref");
3109                current_formula_si =
3110                    raw_attribute(&start, b"si").and_then(|value| value.parse::<u32>().ok());
3111            }
3112            Event::End(end) if in_sheet_data && end.local_name().as_ref() == b"f" => {
3113                in_formula = false;
3114            }
3115            Event::Empty(start) if in_sheet_data && start.local_name().as_ref() == b"f" => {
3116                has_formula = true;
3117                current_formula_type = raw_attribute(&start, b"t");
3118                current_formula_ref = raw_attribute(&start, b"ref");
3119                current_formula_si =
3120                    raw_attribute(&start, b"si").and_then(|value| value.parse::<u32>().ok());
3121            }
3122            Event::Text(text) if in_formula => {
3123                formula_buffer.push_str(&xml_core::decode_text(&text)?);
3124            }
3125
3126            // `<cols>`/`<col>`. This crate's own writer always writes `min == max` (one column per
3127            // entry, see `ColumnSetting`'s doc comment) — an arbitrary real `.xlsx`'s ranged `<col
3128            // min="1" max="3"../>` is still tolerated here by expanding it into one `ColumnSetting`
3129            // per column in the range.
3130            Event::Empty(start) if start.local_name().as_ref() == b"col" => {
3131                let min =
3132                    raw_attribute(&start, b"min").and_then(|value| value.parse::<usize>().ok());
3133                let max =
3134                    raw_attribute(&start, b"max").and_then(|value| value.parse::<usize>().ok());
3135                let width =
3136                    raw_attribute(&start, b"width").and_then(|value| value.parse::<f64>().ok());
3137                let hidden = raw_attribute(&start, b"hidden").as_deref() == Some("1");
3138                let outline_level = raw_attribute(&start, b"outlineLevel")
3139                    .and_then(|value| value.parse::<u8>().ok())
3140                    .unwrap_or(0);
3141                // `<col collapsed="1">`.
3142                let collapsed = raw_attribute(&start, b"collapsed").as_deref() == Some("1");
3143                // `<col style="n">` — point 49, resolved through `cell_formats` exactly like a
3144                // cell's own `s` attribute.
3145                let style = raw_attribute(&start, b"style")
3146                    .and_then(|value| value.parse::<usize>().ok())
3147                    .and_then(|index| cell_formats.get(index).cloned().flatten());
3148                if let (Some(min), Some(max)) = (min, max) {
3149                    // `min`/`max` come straight from the file's own `<col min=".." max="..">` — a
3150                    // malformed `min="0"` would underflow `column_number - 1` below, and an
3151                    // unbounded `max` (or an inverted `min` > `max`) would try to push millions of
3152                    // `ColumnSetting`s for a single `<col>` element. Clamp both to Excel's own real
3153                    // column range instead of trusting the file.
3154                    let min = min.max(1);
3155                    let max = max.min(EXCEL_MAX_COLUMNS);
3156                    if min <= max {
3157                        for column_number in min..=max {
3158                            let mut setting = ColumnSetting::new(column_number - 1)
3159                                .with_hidden(hidden)
3160                                .with_outline_level(outline_level)
3161                                .with_collapsed(collapsed);
3162                            if let Some(width) = width {
3163                                setting = setting.with_width(width);
3164                            }
3165                            if let Some(style) = style.clone() {
3166                                setting = setting.with_style(style);
3167                            }
3168                            column_settings.push(setting);
3169                        }
3170                    }
3171                }
3172            }
3173
3174            // `<mergeCells>`/`<mergeCell>` — point 2.
3175            Event::Empty(start) if start.local_name().as_ref() == b"mergeCell" => {
3176                if let Some(range) = raw_attribute(&start, b"ref") {
3177                    merged_ranges.push(range);
3178                }
3179            }
3180
3181            // `<autoFilter>` — point 6.
3182            Event::Start(start) | Event::Empty(start)
3183                if start.local_name().as_ref() == b"autoFilter" =>
3184            {
3185                auto_filter_range = raw_attribute(&start, b"ref");
3186            }
3187
3188            // `<sheetProtection>` — point 13. See `SheetProtection.password`'s doc comment for why
3189            // the password itself is never recovered.
3190            Event::Start(start) | Event::Empty(start)
3191                if start.local_name().as_ref() == b"sheetProtection" =>
3192            {
3193                if raw_attribute(&start, b"sheet").as_deref() == Some("1") {
3194                    protection = Some(SheetProtection::locked());
3195                }
3196            }
3197
3198            // `<pane>` inside `<sheetViews><sheetView>` — point 9. Only `state="frozen"` is modeled
3199            // (matches the writer).
3200            Event::Empty(start) if start.local_name().as_ref() == b"pane" => {
3201                if raw_attribute(&start, b"state").as_deref() == Some("frozen") {
3202                    let frozen_columns = raw_attribute(&start, b"xSplit")
3203                        .and_then(|value| value.parse::<usize>().ok())
3204                        .unwrap_or(0);
3205                    let frozen_rows = raw_attribute(&start, b"ySplit")
3206                        .and_then(|value| value.parse::<usize>().ok())
3207                        .unwrap_or(0);
3208                    freeze_panes = Some(FreezePane::new(frozen_columns, frozen_rows));
3209                }
3210            }
3211
3212            // `<selection activeCell="..">` — point 40. A real file may repeat `<selection>` once
3213            // per pane quadrant when frozen panes are in play; this crate models only one active
3214            // cell per sheet (see `Sheet.active_cell`'s doc comment), so the first `<selection>`
3215            // encountered wins and later ones are ignored.
3216            Event::Empty(start)
3217                if active_cell.is_none() && start.local_name().as_ref() == b"selection" =>
3218            {
3219                active_cell = raw_attribute(&start, b"activeCell");
3220            }
3221
3222            // `<sheetPr><tabColor rgb="..">` — point 38.
3223            Event::Empty(start) if start.local_name().as_ref() == b"tabColor" => {
3224                tab_color = raw_attribute(&start, b"rgb");
3225            }
3226
3227            // `<sheetPr><outlinePr summaryBelow=".." summaryRight="..">`.
3228            Event::Empty(start) if start.local_name().as_ref() == b"outlinePr" => {
3229                outline_summary_below =
3230                    raw_attribute(&start, b"summaryBelow").map(|value| value == "1");
3231                outline_summary_right =
3232                    raw_attribute(&start, b"summaryRight").map(|value| value == "1");
3233            }
3234
3235            // `<sheetFormatPr defaultColWidth defaultRowHeight>` — point 41.
3236            Event::Start(start) | Event::Empty(start)
3237                if start.local_name().as_ref() == b"sheetFormatPr" =>
3238            {
3239                default_column_width = raw_attribute(&start, b"defaultColWidth")
3240                    .and_then(|value| value.parse::<f64>().ok());
3241                default_row_height = raw_attribute(&start, b"defaultRowHeight")
3242                    .and_then(|value| value.parse::<f64>().ok());
3243            }
3244
3245            // `<sheetView zoomScale="..">` — point 29, extended for
3246            // `showGridLines`/`showRowColHeaders`/`showZeros`/`rightToLeft`. Each attribute
3247            // defaults to `"true"` per `CT_SheetView`'s own schema when absent, but this crate
3248            // keeps `None` (Excel's own default, not written) rather than resolving the absent case
3249            // to `Some(true)` — matches every other `Option<bool>` field's convention elsewhere in
3250            // this crate.
3251            Event::Start(start) | Event::Empty(start)
3252                if start.local_name().as_ref() == b"sheetView" =>
3253            {
3254                zoom_scale =
3255                    raw_attribute(&start, b"zoomScale").and_then(|value| value.parse::<u32>().ok());
3256                // Same strict `"1"` convention as every other `Option<bool>` attribute elsewhere in
3257                // this file (e.g. `hidden`/ `allowBlank` above) — an absent attribute resolves to
3258                // `None` (via `Option::map`), not `Some(true)`, even though `ST_Boolean`'s own
3259                // schema default for these four attributes happens to be `"true"`.
3260                show_grid_lines = raw_attribute(&start, b"showGridLines")
3261                    .as_deref()
3262                    .map(|value| value == "1");
3263                show_row_col_headers = raw_attribute(&start, b"showRowColHeaders")
3264                    .as_deref()
3265                    .map(|value| value == "1");
3266                show_zeros = raw_attribute(&start, b"showZeros")
3267                    .as_deref()
3268                    .map(|value| value == "1");
3269                right_to_left = raw_attribute(&start, b"rightToLeft")
3270                    .as_deref()
3271                    .map(|value| value == "1");
3272            }
3273
3274            // `<rowBreaks>`/`<colBreaks>`/`<brk>` — point 30.
3275            Event::Start(start) if start.local_name().as_ref() == b"rowBreaks" => {
3276                in_row_breaks = true;
3277            }
3278            Event::End(end) if end.local_name().as_ref() == b"rowBreaks" => {
3279                in_row_breaks = false;
3280            }
3281            Event::Start(start) if start.local_name().as_ref() == b"colBreaks" => {
3282                in_col_breaks = true;
3283            }
3284            Event::End(end) if end.local_name().as_ref() == b"colBreaks" => {
3285                in_col_breaks = false;
3286            }
3287            Event::Empty(start)
3288                if (in_row_breaks || in_col_breaks) && start.local_name().as_ref() == b"brk" =>
3289            {
3290                if let Some(id) =
3291                    raw_attribute(&start, b"id").and_then(|value| value.parse::<u32>().ok())
3292                {
3293                    if in_row_breaks {
3294                        row_breaks.push(id);
3295                    } else {
3296                        column_breaks.push(id);
3297                    }
3298                }
3299            }
3300
3301            // `<protectedRanges>`/`<protectedRange>`.
3302            Event::Empty(start) if start.local_name().as_ref() == b"protectedRange" => {
3303                if let (Some(name), Some(range)) = (
3304                    raw_attribute(&start, b"name"),
3305                    raw_attribute(&start, b"sqref"),
3306                ) {
3307                    protected_ranges.push(ProtectedRange::new(name, range));
3308                }
3309            }
3310
3311            // `<ignoredErrors>`/`<ignoredError>`. `sqref` is the only required attribute
3312            // (`CT_IgnoredError`, confirmed via `sml.xsd`); every flag attribute defaults to
3313            // `false` when absent, matching this crate's usual strict `"1"` convention for
3314            // `Option<bool>`/ `bool` attributes elsewhere in this file.
3315            Event::Empty(start) if start.local_name().as_ref() == b"ignoredError" => {
3316                if let Some(range) = raw_attribute(&start, b"sqref") {
3317                    let mut entry = IgnoredError::new(range);
3318                    entry.eval_error = raw_attribute(&start, b"evalError").as_deref() == Some("1");
3319                    entry.two_digit_text_year =
3320                        raw_attribute(&start, b"twoDigitTextYear").as_deref() == Some("1");
3321                    entry.number_stored_as_text =
3322                        raw_attribute(&start, b"numberStoredAsText").as_deref() == Some("1");
3323                    entry.formula = raw_attribute(&start, b"formula").as_deref() == Some("1");
3324                    entry.formula_range =
3325                        raw_attribute(&start, b"formulaRange").as_deref() == Some("1");
3326                    entry.unlocked_formula =
3327                        raw_attribute(&start, b"unlockedFormula").as_deref() == Some("1");
3328                    entry.empty_cell_reference =
3329                        raw_attribute(&start, b"emptyCellReference").as_deref() == Some("1");
3330                    entry.list_data_validation =
3331                        raw_attribute(&start, b"listDataValidation").as_deref() == Some("1");
3332                    entry.calculated_column =
3333                        raw_attribute(&start, b"calculatedColumn").as_deref() == Some("1");
3334                    ignored_errors.push(entry);
3335                }
3336            }
3337
3338            // `<scenarios>`/`<scenario>`/`<inputCells>` — point 35.
3339            Event::Start(start) if start.local_name().as_ref() == b"scenario" => {
3340                let mut scenario =
3341                    Scenario::new(raw_attribute(&start, b"name").unwrap_or_default());
3342                scenario.comment = raw_attribute(&start, b"comment");
3343                current_scenario = Some(scenario);
3344            }
3345            Event::End(end) if end.local_name().as_ref() == b"scenario" => {
3346                if let Some(scenario) = current_scenario.take() {
3347                    scenarios.push(scenario);
3348                }
3349            }
3350            Event::Empty(start)
3351                if current_scenario.is_some() && start.local_name().as_ref() == b"inputCells" =>
3352            {
3353                if let (Some(cell), Some(value)) =
3354                    (raw_attribute(&start, b"r"), raw_attribute(&start, b"val"))
3355                {
3356                    if let Some(scenario) = current_scenario.as_mut() {
3357                        scenario.inputs.push((cell, value));
3358                    }
3359                }
3360            }
3361
3362            // `<filterColumn>`/`<filters>`/`<filter>` — point 36, children of an already-parsed
3363            // `<autoFilter>` (see below).
3364            Event::Start(start) if start.local_name().as_ref() == b"filterColumn" => {
3365                let column_offset = raw_attribute(&start, b"colId")
3366                    .and_then(|value| value.parse::<u32>().ok())
3367                    .unwrap_or(0);
3368                current_filter_column = Some(AutoFilterColumn::new(column_offset, Vec::new()));
3369            }
3370            Event::End(end) if end.local_name().as_ref() == b"filterColumn" => {
3371                if let Some(column) = current_filter_column.take() {
3372                    auto_filter_columns.push(column);
3373                }
3374            }
3375            Event::Empty(start)
3376                if current_filter_column.is_some() && start.local_name().as_ref() == b"filter" =>
3377            {
3378                if let Some(value) = raw_attribute(&start, b"val") {
3379                    if let Some(column) = current_filter_column.as_mut() {
3380                        column.visible_values.push(value);
3381                    }
3382                }
3383            }
3384
3385            // `<sortState ref="..">`/`<sortCondition ref=".." descending="..">` — point 53, nested
3386            // inside this worksheet-level `<autoFilter>`.
3387            Event::Start(start) if start.local_name().as_ref() == b"sortState" => {
3388                current_sort_state = Some(SortState::new(
3389                    raw_attribute(&start, b"ref").unwrap_or_default(),
3390                ));
3391            }
3392            Event::Empty(start) if start.local_name().as_ref() == b"sortState" => {
3393                sort_state = Some(SortState::new(
3394                    raw_attribute(&start, b"ref").unwrap_or_default(),
3395                ));
3396            }
3397            Event::End(end) if end.local_name().as_ref() == b"sortState" => {
3398                sort_state = current_sort_state.take();
3399            }
3400            Event::Empty(start)
3401                if current_sort_state.is_some()
3402                    && start.local_name().as_ref() == b"sortCondition" =>
3403            {
3404                if let Some(range) = raw_attribute(&start, b"ref") {
3405                    let descending = raw_attribute(&start, b"descending").as_deref() == Some("1");
3406                    if let Some(state) = current_sort_state.as_mut() {
3407                        let mut condition = SortCondition::new(range);
3408                        condition.descending = descending;
3409                        state.conditions.push(condition);
3410                    }
3411                }
3412            }
3413
3414            // `<pageSetup>` — point 12's read side, extended for `scale`.
3415            Event::Start(start) | Event::Empty(start)
3416                if start.local_name().as_ref() == b"pageSetup" =>
3417            {
3418                orientation = raw_attribute(&start, b"orientation")
3419                    .as_deref()
3420                    .and_then(parse_page_orientation);
3421                fit_to_width = raw_attribute(&start, b"fitToWidth")
3422                    .and_then(|value| value.parse::<u32>().ok());
3423                fit_to_height = raw_attribute(&start, b"fitToHeight")
3424                    .and_then(|value| value.parse::<u32>().ok());
3425                scale = raw_attribute(&start, b"scale").and_then(|value| value.parse::<u32>().ok());
3426                // `"0"` means "unconstrained" for whichever dimension the caller didn't actually
3427                // ask to fit (see `PrintSettings::fit_to_width`'s doc comment) — reads back as
3428                // `None`, not `Some(0)`.
3429                if fit_to_width == Some(0) {
3430                    fit_to_width = None;
3431                }
3432                if fit_to_height == Some(0) {
3433                    fit_to_height = None;
3434                }
3435            }
3436
3437            // `<headerFooter>`/`<oddHeader>`/. — point 12's read side, extended for point 31's
3438            // first/even variants.
3439            Event::Start(start) if start.local_name().as_ref() == b"oddHeader" => {
3440                in_header_footer_element = Some("oddHeader");
3441                header_footer_buffer.clear();
3442            }
3443            Event::Start(start) if start.local_name().as_ref() == b"oddFooter" => {
3444                in_header_footer_element = Some("oddFooter");
3445                header_footer_buffer.clear();
3446            }
3447            Event::Start(start) if start.local_name().as_ref() == b"evenHeader" => {
3448                in_header_footer_element = Some("evenHeader");
3449                header_footer_buffer.clear();
3450            }
3451            Event::Start(start) if start.local_name().as_ref() == b"evenFooter" => {
3452                in_header_footer_element = Some("evenFooter");
3453                header_footer_buffer.clear();
3454            }
3455            Event::Start(start) if start.local_name().as_ref() == b"firstHeader" => {
3456                in_header_footer_element = Some("firstHeader");
3457                header_footer_buffer.clear();
3458            }
3459            Event::Start(start) if start.local_name().as_ref() == b"firstFooter" => {
3460                in_header_footer_element = Some("firstFooter");
3461                header_footer_buffer.clear();
3462            }
3463            Event::Text(text) if in_header_footer_element.is_some() => {
3464                header_footer_buffer.push_str(&xml_core::decode_text(&text)?);
3465            }
3466            Event::GeneralRef(reference) if in_header_footer_element.is_some() => {
3467                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
3468                    header_footer_buffer.push_str(&resolved);
3469                }
3470            }
3471            Event::End(end)
3472                if in_header_footer_element.is_some()
3473                    && matches!(
3474                        end.local_name().as_ref(),
3475                        b"oddHeader"
3476                            | b"oddFooter"
3477                            | b"evenHeader"
3478                            | b"evenFooter"
3479                            | b"firstHeader"
3480                            | b"firstFooter"
3481                    ) =>
3482            {
3483                let text = std::mem::take(&mut header_footer_buffer);
3484                match in_header_footer_element.take() {
3485                    Some("oddHeader") => header = Some(text),
3486                    Some("oddFooter") => footer = Some(text),
3487                    Some("evenHeader") => header_even = Some(text),
3488                    Some("evenFooter") => footer_even = Some(text),
3489                    Some("firstHeader") => header_first = Some(text),
3490                    Some("firstFooter") => footer_first = Some(text),
3491                    _ => {}
3492                }
3493            }
3494
3495            // `<hyperlinks>`/`<hyperlink>` — point 10. An external link's `r:id` is resolved
3496            // against this worksheet's own relationships (`sheet_relationships`, passed in by
3497            // `read_from`) back to its URL; an internal link carries its `location` directly, no
3498            // relationship needed.
3499            Event::Start(start) | Event::Empty(start)
3500                if start.local_name().as_ref() == b"hyperlink" =>
3501            {
3502                if let Some(cell) = raw_attribute(&start, b"ref") {
3503                    let tooltip = raw_attribute(&start, b"tooltip");
3504                    let target = if let Some(location) = raw_attribute(&start, b"location") {
3505                        Some(HyperlinkTarget::Internal(location))
3506                    } else {
3507                        raw_attribute(&start, b"id")
3508                            .and_then(|relationship_id| sheet_relationships.by_id(&relationship_id))
3509                            .map(|relationship| {
3510                                HyperlinkTarget::External(relationship.target.clone())
3511                            })
3512                    };
3513                    if let Some(target) = target {
3514                        hyperlinks.push(CellHyperlink {
3515                            cell,
3516                            target,
3517                            tooltip,
3518                        });
3519                    }
3520                }
3521            }
3522
3523            Event::Start(start) if start.local_name().as_ref() == b"conditionalFormatting" => {
3524                current_cf_sqref = raw_attribute(&start, b"sqref");
3525            }
3526            Event::End(end) if end.local_name().as_ref() == b"conditionalFormatting" => {
3527                current_cf_sqref = None;
3528            }
3529
3530            Event::Start(start) if start.local_name().as_ref() == b"cfRule" => {
3531                in_cf_rule = true;
3532                cf_rule_type = raw_attribute(&start, b"type");
3533                cf_rule_dxf_id =
3534                    raw_attribute(&start, b"dxfId").and_then(|value| value.parse::<usize>().ok());
3535                cf_rule_operator = raw_attribute(&start, b"operator");
3536                cf_rule_text = raw_attribute(&start, b"text");
3537                cf_formulas.clear();
3538            }
3539            // A self-closing `<cfRule../>` (`top10`/`aboveAverage`/
3540            // `duplicateValues`/`uniqueValues` — this crate's own writer always writes these with
3541            // no children, see `writer.rs::write_conditional_formatting`) has no matching `End`
3542            // event, so it's resolved immediately here instead of going through the
3543            // `in_cf_rule`/`End` path below.
3544            Event::Empty(start) if start.local_name().as_ref() == b"cfRule" => {
3545                let rule_type = raw_attribute(&start, b"type");
3546                let dxf_id =
3547                    raw_attribute(&start, b"dxfId").and_then(|value| value.parse::<usize>().ok());
3548                if let Some(sqref) = current_cf_sqref.clone() {
3549                    let format = dxf_id
3550                        .and_then(|id| dxfs.get(id).cloned())
3551                        .unwrap_or_default();
3552                    match rule_type.as_deref() {
3553                        Some("top10") => {
3554                            let rank = raw_attribute(&start, b"rank")
3555                                .and_then(|value| value.parse::<u32>().ok())
3556                                .unwrap_or(10);
3557                            let percent = raw_attribute(&start, b"percent").as_deref() == Some("1");
3558                            let bottom = raw_attribute(&start, b"bottom").as_deref() == Some("1");
3559                            let rule =
3560                                ConditionalFormattingRule::top10(sqref, rank, percent, format)
3561                                    .with_bottom(bottom);
3562                            conditional_formatting_rules.push(rule);
3563                        }
3564                        Some("aboveAverage") => {
3565                            let above =
3566                                raw_attribute(&start, b"aboveAverage").as_deref() != Some("0");
3567                            conditional_formatting_rules.push(
3568                                ConditionalFormattingRule::above_average(sqref, above, format),
3569                            );
3570                        }
3571                        Some("duplicateValues") => {
3572                            conditional_formatting_rules
3573                                .push(ConditionalFormattingRule::duplicate_values(sqref, format));
3574                        }
3575                        Some("uniqueValues") => {
3576                            conditional_formatting_rules
3577                                .push(ConditionalFormattingRule::unique_values(sqref, format));
3578                        }
3579                        _ => {}
3580                    }
3581                }
3582            }
3583            Event::End(end) if in_cf_rule && end.local_name().as_ref() == b"cfRule" => {
3584                in_cf_rule = false;
3585                if let Some(sqref) = current_cf_sqref.clone() {
3586                    let format = cf_rule_dxf_id
3587                        .take()
3588                        .and_then(|id| dxfs.get(id).cloned())
3589                        .unwrap_or_default();
3590                    let mut formulas = std::mem::take(&mut cf_formulas).into_iter();
3591                    match cf_rule_type.take().as_deref() {
3592                        Some("cellIs") => {
3593                            if let Some(operator) = cf_rule_operator
3594                                .take()
3595                                .as_deref()
3596                                .and_then(parse_comparison_operator)
3597                            {
3598                                if let Some(formula1) = formulas.next() {
3599                                    let mut rule = ConditionalFormattingRule::cell_is(
3600                                        sqref, operator, formula1, format,
3601                                    );
3602                                    if let Some(formula2) = formulas.next() {
3603                                        rule = rule.with_second_formula(formula2);
3604                                    }
3605                                    conditional_formatting_rules.push(rule);
3606                                }
3607                            }
3608                        }
3609                        Some("expression") => {
3610                            if let Some(formula) = formulas.next() {
3611                                conditional_formatting_rules.push(
3612                                    ConditionalFormattingRule::expression(sqref, formula, format),
3613                                );
3614                            }
3615                        }
3616                        // `colorScale`/`dataBar`/`iconSet` are write-only — not reconstructed on
3617                        // read (their children `<colorScale>`/`<dataBar>`/`<iconSet>` aren't parsed
3618                        // at all).
3619                        Some(
3620                            text_type @ ("containsText" | "notContainsText" | "beginsWith"
3621                            | "endsWith"),
3622                        ) => {
3623                            if let Some(operator) = text_comparison_operator_from_type(text_type) {
3624                                if let Some(text) = cf_rule_text.take() {
3625                                    conditional_formatting_rules.push(
3626                                        ConditionalFormattingRule::contains_text(
3627                                            sqref, operator, text, format,
3628                                        ),
3629                                    );
3630                                }
3631                            }
3632                        }
3633                        _ => {}
3634                    }
3635                }
3636                cf_rule_operator = None;
3637                cf_rule_text = None;
3638            }
3639            Event::Start(start) if in_cf_rule && start.local_name().as_ref() == b"formula" => {
3640                in_cf_formula = true;
3641                cf_formula_buffer.clear();
3642            }
3643            Event::End(end) if in_cf_rule && end.local_name().as_ref() == b"formula" => {
3644                in_cf_formula = false;
3645                cf_formulas.push(std::mem::take(&mut cf_formula_buffer));
3646            }
3647            Event::Text(text) if in_cf_formula => {
3648                cf_formula_buffer.push_str(&xml_core::decode_text(&text)?);
3649            }
3650            // `quick_xml` 0.41 splits every `&entity;`/`&#NNN;` reference out of a text node into
3651            // its own `GeneralRef` event, separate from the surrounding `Text` event(s) — a
3652            // formula/source string containing `"` (written escaped as `&quot;`, e.g. a `list`
3653            // validation's quoted literal source) would otherwise silently lose that character on
3654            // read, the same class of bug already fixed for `word-ooxml`'s paragraph text (see
3655            // `CHANGELOG.md`).
3656            Event::GeneralRef(reference) if in_cf_formula => {
3657                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
3658                    cf_formula_buffer.push_str(&resolved);
3659                }
3660            }
3661
3662            Event::Start(start) if start.local_name().as_ref() == b"dataValidation" => {
3663                in_data_validation = true;
3664                dv_type = raw_attribute(&start, b"type");
3665                dv_operator = raw_attribute(&start, b"operator");
3666                dv_allow_blank = raw_attribute(&start, b"allowBlank").as_deref() == Some("1");
3667                dv_show_input_message =
3668                    raw_attribute(&start, b"showInputMessage").as_deref() == Some("1");
3669                dv_show_error_message =
3670                    raw_attribute(&start, b"showErrorMessage").as_deref() == Some("1");
3671                dv_prompt_title = raw_attribute(&start, b"promptTitle");
3672                dv_prompt = raw_attribute(&start, b"prompt");
3673                dv_error_title = raw_attribute(&start, b"errorTitle");
3674                dv_error = raw_attribute(&start, b"error");
3675                dv_sqref = raw_attribute(&start, b"sqref");
3676                dv_formula1 = None;
3677                dv_formula2 = None;
3678            }
3679            Event::End(end) if end.local_name().as_ref() == b"dataValidation" => {
3680                in_data_validation = false;
3681                if let (Some(sqref), Some(formula1)) = (dv_sqref.take(), dv_formula1.take()) {
3682                    let kind = match dv_type.take().as_deref() {
3683                        Some("list") => Some(DataValidationKind::List { source: formula1 }),
3684                        Some("whole") => dv_operator
3685                            .as_deref()
3686                            .and_then(parse_comparison_operator)
3687                            .map(|operator| DataValidationKind::WholeNumber {
3688                                operator,
3689                                formula1,
3690                                formula2: dv_formula2.clone(),
3691                            }),
3692                        Some("decimal") => dv_operator
3693                            .as_deref()
3694                            .and_then(parse_comparison_operator)
3695                            .map(|operator| DataValidationKind::Decimal {
3696                                operator,
3697                                formula1,
3698                                formula2: dv_formula2.clone(),
3699                            }),
3700                        Some("date") => dv_operator
3701                            .as_deref()
3702                            .and_then(parse_comparison_operator)
3703                            .map(|operator| DataValidationKind::Date {
3704                                operator,
3705                                formula1,
3706                                formula2: dv_formula2.clone(),
3707                            }),
3708                        Some("time") => dv_operator
3709                            .as_deref()
3710                            .and_then(parse_comparison_operator)
3711                            .map(|operator| DataValidationKind::Time {
3712                                operator,
3713                                formula1,
3714                                formula2: dv_formula2.clone(),
3715                            }),
3716                        Some("textLength") => dv_operator
3717                            .as_deref()
3718                            .and_then(parse_comparison_operator)
3719                            .map(|operator| DataValidationKind::TextLength {
3720                                operator,
3721                                formula1,
3722                                formula2: dv_formula2.clone(),
3723                            }),
3724                        Some("custom") => Some(DataValidationKind::Custom { formula: formula1 }),
3725                        _ => None,
3726                    };
3727                    if let Some(kind) = kind {
3728                        data_validation_rules.push(DataValidationRule {
3729                            range: sqref,
3730                            kind,
3731                            allow_blank: dv_allow_blank,
3732                            input_message: if dv_show_input_message {
3733                                Some(Message {
3734                                    title: dv_prompt_title.take(),
3735                                    text: dv_prompt.take().unwrap_or_default(),
3736                                })
3737                            } else {
3738                                None
3739                            },
3740                            error_message: if dv_show_error_message {
3741                                Some(Message {
3742                                    title: dv_error_title.take(),
3743                                    text: dv_error.take().unwrap_or_default(),
3744                                })
3745                            } else {
3746                                None
3747                            },
3748                        });
3749                    }
3750                }
3751                dv_operator = None;
3752                dv_formula2 = None;
3753            }
3754            Event::Start(start)
3755                if in_data_validation && start.local_name().as_ref() == b"formula1" =>
3756            {
3757                in_dv_formula1 = true;
3758                dv_formula_buffer.clear();
3759            }
3760            Event::End(end) if in_data_validation && end.local_name().as_ref() == b"formula1" => {
3761                in_dv_formula1 = false;
3762                dv_formula1 = Some(std::mem::take(&mut dv_formula_buffer));
3763            }
3764            Event::Start(start)
3765                if in_data_validation && start.local_name().as_ref() == b"formula2" =>
3766            {
3767                in_dv_formula2 = true;
3768                dv_formula_buffer.clear();
3769            }
3770            Event::End(end) if in_data_validation && end.local_name().as_ref() == b"formula2" => {
3771                in_dv_formula2 = false;
3772                dv_formula2 = Some(std::mem::take(&mut dv_formula_buffer));
3773            }
3774            Event::Text(text) if in_dv_formula1 || in_dv_formula2 => {
3775                dv_formula_buffer.push_str(&xml_core::decode_text(&text)?);
3776            }
3777            // Same `GeneralRef` split as `cf_formula_buffer` above — a `list` validation's quoted
3778            // literal source (`"Oui,Non"`, written escaped as `&quot;Oui,Non&quot;`) would
3779            // otherwise lose its quote characters on read.
3780            Event::GeneralRef(reference) if in_dv_formula1 || in_dv_formula2 => {
3781                if let Some(resolved) = xml_core::decode_general_ref(&reference)? {
3782                    dv_formula_buffer.push_str(&resolved);
3783                }
3784            }
3785
3786            _ => {}
3787        }
3788    }
3789
3790    let print_settings = PrintSettings {
3791        print_area: None,
3792        repeat_rows: None,
3793        repeat_columns: None,
3794        fit_to_width,
3795        fit_to_height,
3796        orientation,
3797        header,
3798        footer,
3799        header_first,
3800        footer_first,
3801        header_even,
3802        footer_even,
3803        scale,
3804    };
3805
3806    Ok(ParsedWorksheet {
3807        rows,
3808        conditional_formatting_rules,
3809        data_validation_rules,
3810        merged_ranges,
3811        column_settings,
3812        auto_filter_range,
3813        freeze_panes,
3814        protection,
3815        hyperlinks,
3816        zoom_scale,
3817        row_breaks,
3818        column_breaks,
3819        scenarios,
3820        auto_filter_columns,
3821        print_settings,
3822        tab_color,
3823        show_grid_lines,
3824        show_row_col_headers,
3825        show_zeros,
3826        right_to_left,
3827        active_cell,
3828        default_column_width,
3829        default_row_height,
3830        ignored_errors,
3831        protected_ranges,
3832        sort_state,
3833        outline_summary_below,
3834        outline_summary_right,
3835    })
3836}
3837
3838/// Grows `items` with `default()`-constructed entries until it has at least `len` elements — used
3839/// to materialize the rows/cells a sparse real-world `.xlsx` skips over entirely.
3840fn ensure_len<T>(items: &mut Vec<T>, len: usize, default: fn() -> T) {
3841    while items.len() < len {
3842        items.push(default());
3843    }
3844}
3845
3846/// Extracts a cell reference's (`"C5"`, `"AA11"`..) column letters and converts them to a 0-based
3847/// column index — the inverse of `writer.rs`'s `column_letters`. Ignores the trailing row-number
3848/// digits entirely (the enclosing `<row r="..">` is the authoritative row number, see
3849/// `parse_worksheet_xml`).
3850fn column_index_from_reference(reference: &str) -> Option<usize> {
3851    let letters: String = reference
3852        .chars()
3853        .take_while(|character| character.is_ascii_alphabetic())
3854        .collect();
3855    // A real column reference is at most 3 letters (`"XFD"` = column 16,384, `EXCEL_MAX_COLUMNS`,
3856    // Excel's own maximum) — rejected up front rather than looping over an attacker-controlled
3857    // number of letters from a malformed `r=".."` and risking the accumulation below overflowing
3858    // `usize`.
3859    if letters.is_empty() || letters.len() > 3 {
3860        return None;
3861    }
3862
3863    let mut index: usize = 0;
3864    for character in letters.chars() {
3865        let digit = (character.to_ascii_uppercase() as u8 - b'A' + 1) as usize;
3866        index = index.checked_mul(26)?.checked_add(digit)?;
3867    }
3868
3869    let column_index = index.checked_sub(1)?;
3870    (column_index < EXCEL_MAX_COLUMNS).then_some(column_index)
3871}
3872
3873/// Reads a single attribute's decoded value by local name (ignoring any namespace prefix, e.g.
3874/// `b"id"` matches both `id=".."` and `r:id=".."`) — mirrors `word-ooxml`'s own `raw_attribute`
3875/// helper.
3876fn raw_attribute(start: &BytesStart, name: &[u8]) -> Option<String> {
3877    start
3878        .attributes()
3879        .flatten()
3880        .find(|attribute| attribute.key.local_name().as_ref() == name)
3881        .and_then(|attribute| {
3882            xml_core::decode_attribute_value(attribute.value.as_ref())
3883                .ok()
3884                .flatten()
3885        })
3886}
3887
3888#[cfg(test)]
3889mod tests {
3890    use super::*;
3891
3892    #[test]
3893    fn parses_sheet_names_and_relationship_ids_from_workbook_xml() {
3894        let xml = r#"<?xml version="1.0"?>
3895<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3896  <sheets>
3897    <sheet name="Feuil1" sheetId="1" r:id="rId1"/>
3898    <sheet name="Feuil2" sheetId="2" r:id="rId2"/>
3899  </sheets>
3900</workbook>"#;
3901
3902        let parsed = parse_workbook_xml(xml).unwrap();
3903
3904        assert_eq!(
3905            parsed.sheets,
3906            vec![
3907                (
3908                    "Feuil1".to_string(),
3909                    "rId1".to_string(),
3910                    SheetVisibility::Visible
3911                ),
3912                (
3913                    "Feuil2".to_string(),
3914                    "rId2".to_string(),
3915                    SheetVisibility::Visible
3916                ),
3917            ]
3918        );
3919        assert!(parsed.defined_names.is_empty());
3920    }
3921
3922    #[test]
3923    fn parses_defined_names_from_workbook_xml() {
3924        let xml = r#"<?xml version="1.0"?>
3925<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
3926  <sheets>
3927    <sheet name="Feuil1" sheetId="1" r:id="rId1"/>
3928    <sheet name="Feuil2" sheetId="2" r:id="rId2"/>
3929  </sheets>
3930  <definedNames>
3931    <definedName name="Plage_Ventes">Feuil1!$A$1:$C$12</definedName>
3932    <definedName name="Northwind_Database" localSheetId="1" hidden="1" comment="Source externe">Feuil2!$A$1:$T$47</definedName>
3933  </definedNames>
3934</workbook>"#;
3935
3936        let parsed = parse_workbook_xml(xml).unwrap();
3937        let defined_names = parsed.defined_names;
3938
3939        assert_eq!(defined_names.len(), 2);
3940        assert_eq!(defined_names[0].name, "Plage_Ventes");
3941        assert_eq!(defined_names[0].refers_to, "Feuil1!$A$1:$C$12");
3942        assert_eq!(defined_names[0].local_sheet_id, None);
3943        assert!(!defined_names[0].hidden);
3944        assert_eq!(defined_names[0].comment, None);
3945
3946        assert_eq!(defined_names[1].name, "Northwind_Database");
3947        assert_eq!(defined_names[1].refers_to, "Feuil2!$A$1:$T$47");
3948        assert_eq!(defined_names[1].local_sheet_id, Some(1));
3949        assert!(defined_names[1].hidden);
3950        assert_eq!(defined_names[1].comment.as_deref(), Some("Source externe"));
3951    }
3952
3953    #[test]
3954    fn parses_simple_shared_strings() {
3955        let xml = r#"<?xml version="1.0"?>
3956<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="2" uniqueCount="2">
3957  <si><t>Nombre_1</t></si>
3958  <si><t>Nombre_2</t></si>
3959</sst>"#;
3960
3961        let strings = parse_shared_strings_xml(xml).unwrap();
3962
3963        assert_eq!(
3964            strings,
3965            vec![
3966                SharedStringEntry::Plain("Nombre_1".to_string()),
3967                SharedStringEntry::Plain("Nombre_2".to_string())
3968            ]
3969        );
3970    }
3971
3972    #[test]
3973    fn parses_rich_text_run_shared_strings_into_distinct_runs() {
3974        let xml = r#"<?xml version="1.0"?>
3975<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1">
3976  <si><r><rPr><b/></rPr><t>Hello, </t></r><r><t>world!</t></r></si>
3977</sst>"#;
3978
3979        let strings = parse_shared_strings_xml(xml).unwrap();
3980
3981        assert_eq!(strings.len(), 1);
3982        assert_eq!(
3983            strings[0],
3984            SharedStringEntry::Rich(vec![
3985                RichTextRun::new("Hello, ").with_bold(true),
3986                RichTextRun::new("world!"),
3987            ])
3988        );
3989    }
3990
3991    /// An empty `opc::Relationships` — used by tests that don't exercise hyperlinks (which is most
3992    /// of them; the one that does builds its own).
3993    fn no_relationships() -> opc::Relationships {
3994        opc::Relationships::new()
3995    }
3996
3997    #[test]
3998    fn parses_a_simple_worksheet_with_text_and_numeric_cells() {
3999        let shared_strings = vec![
4000            SharedStringEntry::Plain("Nombre_1".to_string()),
4001            SharedStringEntry::Plain("Nombre_2".to_string()),
4002        ];
4003        let xml = r#"<?xml version="1.0"?>
4004<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4005  <sheetData>
4006    <row r="1"><c r="A1" t="s"><v>0</v></c><c r="B1" t="s"><v>1</v></c></row>
4007    <row r="2"><c r="A2"><v>10</v></c><c r="B2"><v>1</v></c></row>
4008  </sheetData>
4009</worksheet>"#;
4010
4011        let parsed =
4012            parse_worksheet_xml(xml, &shared_strings, &[], &[], &no_relationships()).unwrap();
4013
4014        assert_eq!(parsed.rows.len(), 2);
4015        assert_eq!(
4016            parsed.rows[0].cells[0].value,
4017            CellValue::Text("Nombre_1".to_string())
4018        );
4019        assert_eq!(
4020            parsed.rows[0].cells[1].value,
4021            CellValue::Text("Nombre_2".to_string())
4022        );
4023        assert_eq!(parsed.rows[1].cells[0].value, CellValue::Number(10.0));
4024        assert_eq!(parsed.rows[1].cells[1].value, CellValue::Number(1.0));
4025    }
4026
4027    #[test]
4028    fn fills_in_a_gap_left_by_a_skipped_column_as_an_empty_cell() {
4029        let xml = r#"<?xml version="1.0"?>
4030<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4031  <sheetData>
4032    <row r="1"><c r="A1"><v>1</v></c><c r="C1"><v>3</v></c></row>
4033  </sheetData>
4034</worksheet>"#;
4035
4036        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4037
4038        assert_eq!(parsed.rows[0].cells.len(), 3);
4039        assert_eq!(parsed.rows[0].cells[0].value, CellValue::Number(1.0));
4040        assert_eq!(parsed.rows[0].cells[1].value, CellValue::Empty);
4041        assert_eq!(parsed.rows[0].cells[2].value, CellValue::Number(3.0));
4042    }
4043
4044    #[test]
4045    fn fills_in_a_gap_left_by_a_skipped_row_as_an_empty_row() {
4046        let xml = r#"<?xml version="1.0"?>
4047<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4048  <sheetData>
4049    <row r="1"><c r="A1"><v>1</v></c></row>
4050    <row r="3"><c r="A3"><v>3</v></c></row>
4051  </sheetData>
4052</worksheet>"#;
4053
4054        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4055
4056        assert_eq!(parsed.rows.len(), 3);
4057        assert!(parsed.rows[1].cells.is_empty());
4058        assert_eq!(parsed.rows[2].cells[0].value, CellValue::Number(3.0));
4059    }
4060
4061    #[test]
4062    fn converts_a_cell_reference_to_a_0_based_column_index() {
4063        assert_eq!(column_index_from_reference("A1"), Some(0));
4064        assert_eq!(column_index_from_reference("C5"), Some(2));
4065        assert_eq!(column_index_from_reference("AA11"), Some(26));
4066        assert_eq!(column_index_from_reference(""), None);
4067    }
4068
4069    /// Robustness audit finding: a `.xlsx` a few hundred bytes long could claim a cell reference
4070    /// far beyond Excel's own column limit (`XFD`, column 16,384) — `column_index_from_reference`
4071    /// must reject it rather than overflow its accumulation or hand back an index a caller would
4072    /// then use to size a `Vec`.
4073    #[test]
4074    fn column_index_from_reference_rejects_columns_beyond_excel_s_own_limit() {
4075        // The real maximum ("XFD") still resolves.
4076        assert_eq!(column_index_from_reference("XFD1"), Some(16_383));
4077        // One column past it is rejected, not silently accepted.
4078        assert_eq!(column_index_from_reference("XFE1"), None);
4079        // A reference far outside any real spreadsheet — the kind a malformed/adversarial file
4080        // might carry — is rejected outright rather than looping over its letters at all.
4081        assert_eq!(column_index_from_reference("ZZZZZZZZZZZZZZ1"), None);
4082    }
4083
4084    /// Robustness audit finding: `<row r="..">` is trusted as-is to size `rows` — a malformed row
4085    /// number far beyond Excel's own row limit (1,048,576) must be dropped rather than attempting
4086    /// to allocate a `Vec` that large. This is the whole worksheet, not just the one malformed row:
4087    /// `read_from` must still succeed on the rest of it.
4088    #[test]
4089    fn drops_a_row_whose_number_is_beyond_excel_s_own_limit_instead_of_allocating() {
4090        let xml = r#"<?xml version="1.0"?>
4091<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4092  <sheetData>
4093    <row r="1"><c r="A1"><v>1</v></c></row>
4094    <row r="9999999999"><c r="A1"><v>2</v></c></row>
4095    <row r="2"><c r="A2"><v>3</v></c></row>
4096  </sheetData>
4097</worksheet>"#;
4098
4099        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4100
4101        // Only the two well-formed rows survive; the out-of-range one was dropped instead of
4102        // forcing `rows` to grow to billions of elements.
4103        assert_eq!(parsed.rows.len(), 2);
4104        assert_eq!(parsed.rows[0].cells[0].value, CellValue::Number(1.0));
4105        assert_eq!(parsed.rows[1].cells[0].value, CellValue::Number(3.0));
4106    }
4107
4108    /// Robustness audit finding: `<col min=".." max="..">` is trusted as-is — `min="0"` must not
4109    /// underflow `column_number - 1`, and an unbounded `max` must not push millions of
4110    /// `ColumnSetting`s for one `<col>` element.
4111    #[test]
4112    fn clamps_a_malformed_col_range_to_excel_s_own_column_bounds() {
4113        let xml = r#"<?xml version="1.0"?>
4114<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4115  <cols>
4116    <col min="0" max="2" width="10"/>
4117  </cols>
4118  <sheetData/>
4119</worksheet>"#;
4120
4121        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4122
4123        // `min="0"` is clamped up to `1` rather than underflowing — the resulting range is `1.=2`,
4124        // two columns, not a panic.
4125        assert_eq!(parsed.column_settings.len(), 2);
4126
4127        let xml_unbounded = r#"<?xml version="1.0"?>
4128<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4129  <cols>
4130    <col min="1" max="4000000000" width="10"/>
4131  </cols>
4132  <sheetData/>
4133</worksheet>"#;
4134
4135        let parsed_unbounded =
4136            parse_worksheet_xml(xml_unbounded, &[], &[], &[], &no_relationships()).unwrap();
4137
4138        // Clamped to Excel's real column count (16,384) instead of trying to push four billion
4139        // `ColumnSetting`s.
4140        assert_eq!(parsed_unbounded.column_settings.len(), 16_384);
4141    }
4142
4143    #[test]
4144    fn parses_a_normal_formula_cell_with_a_cached_value() {
4145        let xml = r#"<?xml version="1.0"?>
4146<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4147  <sheetData>
4148    <row r="1"><c r="A1"><f>A2*B2</f><v>10</v></c></row>
4149  </sheetData>
4150</worksheet>"#;
4151
4152        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4153
4154        assert_eq!(
4155            parsed.rows[0].cells[0].value,
4156            CellValue::Formula {
4157                formula: Some("A2*B2".to_string()),
4158                cached_value: Some(10.0)
4159            }
4160        );
4161    }
4162
4163    #[test]
4164    fn parses_a_shared_formula_follower_cell_with_no_literal_text_into_shared_formula() {
4165        // A `t="shared"` cell — even a follower with no literal `<f>` text — reads back as
4166        // `CellValue::SharedFormula`, not `CellValue::Formula { formula: None }` (which would drop
4167        // the `si`/`ref` entirely).
4168        let xml = r#"<?xml version="1.0"?>
4169<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4170  <sheetData>
4171    <row r="1"><c r="C4"><f t="shared" si="0"/><v>30</v></c></row>
4172  </sheetData>
4173</worksheet>"#;
4174
4175        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4176
4177        assert_eq!(
4178            parsed.rows[0].cells[2].value,
4179            CellValue::SharedFormula {
4180                formula: None,
4181                group_index: 0,
4182                range: None,
4183                cached_value: Some(30.0)
4184            }
4185        );
4186    }
4187
4188    #[test]
4189    fn parses_styles_xml_into_cell_formats_indexed_by_cellxfs_position() {
4190        let xml = r#"<?xml version="1.0"?>
4191<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4192  <numFmts count="1"><numFmt numFmtId="164" formatCode="0.00"/></numFmts>
4193  <fonts count="2">
4194    <font><sz val="11"/><color rgb="FF000000"/><name val="Calibri"/><family val="2"/></font>
4195    <font><b/><sz val="11"/><color rgb="FF000000"/><name val="Calibri"/><family val="2"/></font>
4196  </fonts>
4197  <fills count="3">
4198    <fill><patternFill patternType="none"/></fill>
4199    <fill><patternFill patternType="gray125"/></fill>
4200    <fill><patternFill patternType="solid"><fgColor rgb="FFFFFF00"/></patternFill></fill>
4201  </fills>
4202  <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
4203  <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
4204  <cellXfs count="2">
4205    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
4206    <xf numFmtId="164" fontId="1" fillId="2" borderId="0" xfId="0" applyNumberFormat="1" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="center" vertical="top"/></xf>
4207  </cellXfs>
4208  <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
4209</styleSheet>"#;
4210
4211        let ParsedStyles {
4212            cell_formats: formats,
4213            dxfs,
4214            ..
4215        } = parse_styles_xml(xml).unwrap();
4216
4217        assert_eq!(formats.len(), 2);
4218        assert_eq!(formats[0], None);
4219        assert_eq!(
4220            formats[1],
4221            Some(CellFormat {
4222                number_format: Some("0.00".to_string()),
4223                bold: true,
4224                italic: false,
4225                font_size: Some(11.0),
4226                font_color: Some("FF000000".to_string()),
4227                fill_color: Some("FFFFFF00".to_string()),
4228                horizontal_alignment: Some(HorizontalAlignment::Center),
4229                vertical_alignment: Some(VerticalAlignment::Top),
4230                border: crate::model::Border::default(),
4231                font_name: Some("Calibri".to_string()),
4232                underline: false,
4233                strike: false,
4234                wrap_text: false,
4235                indent: None,
4236                text_rotation: None,
4237                shrink_to_fit: false,
4238                pattern_fill: None,
4239                gradient_fill: None,
4240                locked: None,
4241                formula_hidden: None,
4242                named_style: None,
4243            })
4244        );
4245        assert!(dxfs.is_empty());
4246    }
4247
4248    #[test]
4249    fn parses_dxfs_as_inline_self_contained_cell_formats() {
4250        let xml = r#"<?xml version="1.0"?>
4251<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4252  <fonts count="1"><font><sz val="11"/><color rgb="FF000000"/><name val="Calibri"/><family val="2"/></font></fonts>
4253  <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>
4254  <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
4255  <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
4256  <cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
4257  <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
4258  <dxfs count="1">
4259    <dxf>
4260      <font><b/><color rgb="FF9C0006"/></font>
4261      <fill><patternFill patternType="solid"><bgColor rgb="FFFFC7CE"/></patternFill></fill>
4262    </dxf>
4263  </dxfs>
4264</styleSheet>"#;
4265
4266        let ParsedStyles { dxfs, .. } = parse_styles_xml(xml).unwrap();
4267
4268        assert_eq!(dxfs.len(), 1);
4269        assert_eq!(
4270            dxfs[0],
4271            CellFormat {
4272                number_format: None,
4273                bold: true,
4274                italic: false,
4275                font_size: None,
4276                font_color: Some("FF9C0006".to_string()),
4277                fill_color: Some("FFFFC7CE".to_string()),
4278                horizontal_alignment: None,
4279                vertical_alignment: None,
4280                border: crate::model::Border::default(),
4281                font_name: None,
4282                underline: false,
4283                strike: false,
4284                wrap_text: false,
4285                indent: None,
4286                text_rotation: None,
4287                shrink_to_fit: false,
4288                pattern_fill: None,
4289                gradient_fill: None,
4290                locked: None,
4291                formula_hidden: None,
4292                named_style: None,
4293            }
4294        );
4295    }
4296
4297    #[test]
4298    fn parses_a_cell_is_conditional_formatting_rule_resolving_its_dxf() {
4299        let dxfs = vec![
4300            CellFormat::new()
4301                .with_bold(true)
4302                .with_fill_color("FFFFFF00"),
4303        ];
4304        let xml = r#"<?xml version="1.0"?>
4305<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4306  <sheetData/>
4307  <conditionalFormatting sqref="E3:E9">
4308    <cfRule type="cellIs" dxfId="0" priority="1" operator="greaterThan"><formula>0.5</formula></cfRule>
4309  </conditionalFormatting>
4310</worksheet>"#;
4311
4312        let parsed = parse_worksheet_xml(xml, &[], &[], &dxfs, &no_relationships()).unwrap();
4313        let rules = parsed.conditional_formatting_rules;
4314
4315        assert_eq!(rules.len(), 1);
4316        assert_eq!(rules[0].range, "E3:E9");
4317        assert_eq!(rules[0].format, dxfs[0]);
4318        assert_eq!(
4319            rules[0].condition,
4320            crate::model::ConditionalFormattingCondition::CellIs {
4321                operator: ComparisonOperator::GreaterThan,
4322                formula1: "0.5".to_string(),
4323                formula2: None,
4324            }
4325        );
4326    }
4327
4328    #[test]
4329    fn parses_a_data_validation_list_rule() {
4330        let xml = r#"<?xml version="1.0"?>
4331<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4332  <sheetData/>
4333  <dataValidations count="1">
4334    <dataValidation type="list" allowBlank="1" sqref="B2:B20"><formula1>"Oui,Non"</formula1></dataValidation>
4335  </dataValidations>
4336</worksheet>"#;
4337
4338        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4339        let rules = parsed.data_validation_rules;
4340
4341        assert_eq!(rules.len(), 1);
4342        assert_eq!(rules[0].range, "B2:B20");
4343        assert!(rules[0].allow_blank);
4344        assert_eq!(
4345            rules[0].kind,
4346            DataValidationKind::List {
4347                source: "\"Oui,Non\"".to_string()
4348            }
4349        );
4350    }
4351
4352    #[test]
4353    fn parses_a_whole_number_data_validation_rule_with_messages() {
4354        let xml = r#"<?xml version="1.0"?>
4355<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
4356  <sheetData/>
4357  <dataValidations count="1">
4358    <dataValidation type="whole" operator="greaterThanOrEqual" allowBlank="1" showInputMessage="1" showErrorMessage="1" promptTitle="Saisie" prompt="Entrez un nombre positif" errorTitle="Erreur" error="Valeur invalide" sqref="C2:C20"><formula1>0</formula1></dataValidation>
4359  </dataValidations>
4360</worksheet>"#;
4361
4362        let parsed = parse_worksheet_xml(xml, &[], &[], &[], &no_relationships()).unwrap();
4363        let rules = parsed.data_validation_rules;
4364
4365        assert_eq!(rules.len(), 1);
4366        assert_eq!(
4367            rules[0].kind,
4368            DataValidationKind::WholeNumber {
4369                operator: ComparisonOperator::GreaterThanOrEqual,
4370                formula1: "0".to_string(),
4371                formula2: None,
4372            }
4373        );
4374        let input_message = rules[0].input_message.as_ref().unwrap();
4375        assert_eq!(input_message.title.as_deref(), Some("Saisie"));
4376        assert_eq!(input_message.text, "Entrez un nombre positif");
4377        let error_message = rules[0].error_message.as_ref().unwrap();
4378        assert_eq!(error_message.title.as_deref(), Some("Erreur"));
4379        assert_eq!(error_message.text, "Valeur invalide");
4380    }
4381}