1use std::collections::HashMap;
36
37use serde::{Deserialize, Serialize};
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
48pub struct Document {
49 #[serde(
51 default,
52 skip_serializing_if = "Option::is_none",
53 rename = "documentId"
54 )]
55 pub document_id: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub title: Option<String>,
59 #[serde(
66 default,
67 skip_serializing_if = "Option::is_none",
68 rename = "revisionId"
69 )]
70 pub revision_id: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub body: Option<Body>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub tabs: Vec<Tab>,
78 #[serde(
83 default,
84 skip_serializing_if = "HashMap::is_empty",
85 rename = "namedRanges"
86 )]
87 pub named_ranges: HashMap<String, NamedRanges>,
88 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91 pub headers: HashMap<String, Header>,
92 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
94 pub footers: HashMap<String, Footer>,
95 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
97 pub footnotes: HashMap<String, Footnote>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct ResolvedSegment<'a> {
107 pub segment_id: &'a str,
109 pub content: &'a [StructuralElement],
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub struct ResolvedTab<'a> {
124 pub tab_id: Option<&'a str>,
126 pub title: Option<&'a str>,
128 pub nesting_level: i64,
130 pub body: Option<&'a Body>,
132 headers_map: Option<&'a HashMap<String, Header>>,
133 footers_map: Option<&'a HashMap<String, Footer>>,
134 footnotes_map: Option<&'a HashMap<String, Footnote>>,
135}
136
137impl<'a> ResolvedTab<'a> {
138 #[must_use]
141 pub fn headers(&self) -> Vec<ResolvedSegment<'a>> {
142 self.headers_map
143 .map_or_else(Vec::new, |m| resolve_segments(m, |h| h.content.as_slice()))
144 }
145 #[must_use]
147 pub fn footers(&self) -> Vec<ResolvedSegment<'a>> {
148 self.footers_map
149 .map_or_else(Vec::new, |m| resolve_segments(m, |f| f.content.as_slice()))
150 }
151 #[must_use]
153 pub fn footnotes(&self) -> Vec<ResolvedSegment<'a>> {
154 self.footnotes_map
155 .map_or_else(Vec::new, |m| resolve_segments(m, |f| f.content.as_slice()))
156 }
157}
158
159fn resolve_segments<'a, T>(
161 map: &'a HashMap<String, T>,
162 content: impl Fn(&'a T) -> &'a [StructuralElement],
163) -> Vec<ResolvedSegment<'a>> {
164 let mut out: Vec<ResolvedSegment<'a>> = map
165 .iter()
166 .map(|(id, seg)| ResolvedSegment {
167 segment_id: id.as_str(),
168 content: content(seg),
169 })
170 .collect();
171 out.sort_by_key(|s| s.segment_id);
172 out
173}
174
175impl Document {
176 #[must_use]
178 pub fn title(&self) -> &str {
179 self.title.as_deref().unwrap_or("")
180 }
181
182 #[must_use]
192 pub fn resolved_tabs(&self) -> Vec<ResolvedTab<'_>> {
193 if self.tabs.is_empty() {
194 if self.body.is_none()
195 && self.headers.is_empty()
196 && self.footers.is_empty()
197 && self.footnotes.is_empty()
198 {
199 return Vec::new();
200 }
201 return vec![ResolvedTab {
202 tab_id: None,
203 title: None,
204 nesting_level: 0,
205 body: self.body.as_ref(),
206 headers_map: Some(&self.headers),
207 footers_map: Some(&self.footers),
208 footnotes_map: Some(&self.footnotes),
209 }];
210 }
211 let mut out = Vec::new();
212 for tab in &self.tabs {
213 push_tab(tab, 0, &mut out);
214 }
215 out
216 }
217}
218
219fn push_tab<'a>(tab: &'a Tab, depth: i64, out: &mut Vec<ResolvedTab<'a>>) {
221 let props = tab.tab_properties.as_ref();
222 let doc_tab = tab.document_tab.as_ref();
223 out.push(ResolvedTab {
224 tab_id: props.and_then(|p| p.tab_id.as_deref()),
225 title: props.and_then(|p| p.title.as_deref()),
226 nesting_level: props.and_then(|p| p.nesting_level).unwrap_or(depth),
230 body: doc_tab.and_then(|dt| dt.body.as_ref()),
231 headers_map: doc_tab.map(|dt| &dt.headers),
232 footers_map: doc_tab.map(|dt| &dt.footers),
233 footnotes_map: doc_tab.map(|dt| &dt.footnotes),
234 });
235 for child in &tab.child_tabs {
236 push_tab(child, depth + 1, out);
237 }
238}
239
240#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
242pub struct Tab {
243 #[serde(
245 default,
246 skip_serializing_if = "Option::is_none",
247 rename = "tabProperties"
248 )]
249 pub tab_properties: Option<TabProperties>,
250 #[serde(
252 default,
253 skip_serializing_if = "Option::is_none",
254 rename = "documentTab"
255 )]
256 pub document_tab: Option<DocumentTab>,
257 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "childTabs")]
259 pub child_tabs: Vec<Self>,
260}
261
262#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
264pub struct TabProperties {
265 #[serde(default, skip_serializing_if = "Option::is_none", rename = "tabId")]
268 pub tab_id: Option<String>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
271 pub title: Option<String>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
274 pub index: Option<i64>,
275 #[serde(
277 default,
278 skip_serializing_if = "Option::is_none",
279 rename = "nestingLevel"
280 )]
281 pub nesting_level: Option<i64>,
282 #[serde(
284 default,
285 skip_serializing_if = "Option::is_none",
286 rename = "parentTabId"
287 )]
288 pub parent_tab_id: Option<String>,
289}
290
291#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
293pub struct DocumentTab {
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub body: Option<Body>,
297 #[serde(
299 default,
300 skip_serializing_if = "HashMap::is_empty",
301 rename = "namedRanges"
302 )]
303 pub named_ranges: HashMap<String, NamedRanges>,
304 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
306 pub headers: HashMap<String, Header>,
307 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
309 pub footers: HashMap<String, Footer>,
310 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
312 pub footnotes: HashMap<String, Footnote>,
313}
314
315#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
324pub struct Body {
325 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub content: Vec<StructuralElement>,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
332pub struct Header {
333 #[serde(default, skip_serializing_if = "Vec::is_empty")]
335 pub content: Vec<StructuralElement>,
336}
337
338#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
342pub struct Footer {
343 #[serde(default, skip_serializing_if = "Vec::is_empty")]
345 pub content: Vec<StructuralElement>,
346}
347
348#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
351pub struct Footnote {
352 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub content: Vec<StructuralElement>,
355}
356
357#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
360pub struct StructuralElement {
361 #[serde(
363 default,
364 skip_serializing_if = "Option::is_none",
365 rename = "startIndex"
366 )]
367 pub start_index: Option<i64>,
368 #[serde(default, skip_serializing_if = "Option::is_none", rename = "endIndex")]
370 pub end_index: Option<i64>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub paragraph: Option<Paragraph>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub table: Option<Table>,
377 #[serde(
379 default,
380 skip_serializing_if = "Option::is_none",
381 rename = "tableOfContents"
382 )]
383 pub table_of_contents: Option<TableOfContents>,
384 #[serde(
387 default,
388 skip_serializing_if = "Option::is_none",
389 rename = "sectionBreak"
390 )]
391 pub section_break: Option<serde_json::Value>,
392}
393
394impl StructuralElement {
395 #[must_use]
397 pub fn start_index(&self) -> i64 {
398 self.start_index.unwrap_or(0)
399 }
400
401 #[must_use]
403 pub fn end_index(&self) -> i64 {
404 self.end_index.unwrap_or(0)
405 }
406}
407
408#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
410pub struct Paragraph {
411 #[serde(default, skip_serializing_if = "Vec::is_empty")]
413 pub elements: Vec<ParagraphElement>,
414 #[serde(
416 default,
417 skip_serializing_if = "Option::is_none",
418 rename = "paragraphStyle"
419 )]
420 pub paragraph_style: Option<ParagraphStyle>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub bullet: Option<Bullet>,
424}
425
426impl Paragraph {
427 #[must_use]
437 pub fn text(&self) -> String {
438 self.elements
439 .iter()
440 .filter_map(|el| el.text_run.as_ref())
441 .map(|run| run.content.as_str())
442 .collect()
443 }
444
445 #[must_use]
447 pub fn named_style_type(&self) -> Option<&str> {
448 self.paragraph_style
449 .as_ref()
450 .and_then(|style| style.named_style_type.as_deref())
451 }
452}
453
454#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
456pub struct ParagraphStyle {
457 #[serde(
459 default,
460 skip_serializing_if = "Option::is_none",
461 rename = "namedStyleType"
462 )]
463 pub named_style_type: Option<String>,
464 #[serde(default, skip_serializing_if = "Option::is_none", rename = "headingId")]
466 pub heading_id: Option<String>,
467}
468
469#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
471pub struct Bullet {
472 #[serde(default, skip_serializing_if = "Option::is_none", rename = "listId")]
474 pub list_id: Option<String>,
475 #[serde(
477 default,
478 skip_serializing_if = "Option::is_none",
479 rename = "nestingLevel"
480 )]
481 pub nesting_level: Option<i64>,
482}
483
484#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
491pub struct ParagraphElement {
492 #[serde(
494 default,
495 skip_serializing_if = "Option::is_none",
496 rename = "startIndex"
497 )]
498 pub start_index: Option<i64>,
499 #[serde(default, skip_serializing_if = "Option::is_none", rename = "endIndex")]
501 pub end_index: Option<i64>,
502 #[serde(default, skip_serializing_if = "Option::is_none", rename = "textRun")]
504 pub text_run: Option<TextRun>,
505 #[serde(
509 default,
510 skip_serializing_if = "Option::is_none",
511 rename = "inlineObjectElement"
512 )]
513 pub inline_object_element: Option<InlineObjectElement>,
514 #[serde(default, skip_serializing_if = "Option::is_none", rename = "pageBreak")]
516 pub page_break: Option<serde_json::Value>,
517 #[serde(
519 default,
520 skip_serializing_if = "Option::is_none",
521 rename = "columnBreak"
522 )]
523 pub column_break: Option<serde_json::Value>,
524 #[serde(
526 default,
527 skip_serializing_if = "Option::is_none",
528 rename = "horizontalRule"
529 )]
530 pub horizontal_rule: Option<serde_json::Value>,
531 #[serde(
533 default,
534 skip_serializing_if = "Option::is_none",
535 rename = "footnoteReference"
536 )]
537 pub footnote_reference: Option<serde_json::Value>,
538 #[serde(default, skip_serializing_if = "Option::is_none", rename = "autoText")]
540 pub auto_text: Option<serde_json::Value>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub equation: Option<serde_json::Value>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub person: Option<serde_json::Value>,
547 #[serde(default, skip_serializing_if = "Option::is_none", rename = "richLink")]
549 pub rich_link: Option<serde_json::Value>,
550}
551
552#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
554pub struct TextRun {
555 #[serde(default)]
557 pub content: String,
558}
559
560#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
562pub struct InlineObjectElement {
563 #[serde(
565 default,
566 skip_serializing_if = "Option::is_none",
567 rename = "inlineObjectId"
568 )]
569 pub inline_object_id: Option<String>,
570}
571
572#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
574pub struct Table {
575 #[serde(default, skip_serializing_if = "Option::is_none")]
577 pub rows: Option<i64>,
578 #[serde(default, skip_serializing_if = "Option::is_none")]
580 pub columns: Option<i64>,
581 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "tableRows")]
583 pub table_rows: Vec<TableRow>,
584}
585
586#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
588pub struct TableRow {
589 #[serde(
591 default,
592 skip_serializing_if = "Option::is_none",
593 rename = "startIndex"
594 )]
595 pub start_index: Option<i64>,
596 #[serde(default, skip_serializing_if = "Option::is_none", rename = "endIndex")]
598 pub end_index: Option<i64>,
599 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "tableCells")]
601 pub table_cells: Vec<TableCell>,
602}
603
604#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
606pub struct TableCell {
607 #[serde(
609 default,
610 skip_serializing_if = "Option::is_none",
611 rename = "startIndex"
612 )]
613 pub start_index: Option<i64>,
614 #[serde(default, skip_serializing_if = "Option::is_none", rename = "endIndex")]
616 pub end_index: Option<i64>,
617 #[serde(default, skip_serializing_if = "Vec::is_empty")]
620 pub content: Vec<StructuralElement>,
621}
622
623#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
625pub struct TableOfContents {
626 #[serde(default, skip_serializing_if = "Vec::is_empty")]
628 pub content: Vec<StructuralElement>,
629}
630
631#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
633pub struct NamedRanges {
634 #[serde(default)]
636 pub name: String,
637 #[serde(default, skip_serializing_if = "Vec::is_empty", rename = "namedRanges")]
639 pub named_ranges: Vec<NamedRange>,
640}
641
642#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
644pub struct NamedRange {
645 #[serde(
647 default,
648 skip_serializing_if = "Option::is_none",
649 rename = "namedRangeId"
650 )]
651 pub named_range_id: Option<String>,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub name: Option<String>,
655 #[serde(default, skip_serializing_if = "Vec::is_empty")]
657 pub ranges: Vec<Range>,
658}
659
660#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
662pub struct Range {
663 #[serde(default, skip_serializing_if = "Option::is_none", rename = "segmentId")]
665 pub segment_id: Option<String>,
666 #[serde(default, skip_serializing_if = "Option::is_none", rename = "tabId")]
668 pub tab_id: Option<String>,
669 #[serde(
671 default,
672 skip_serializing_if = "Option::is_none",
673 rename = "startIndex"
674 )]
675 pub start_index: Option<i64>,
676 #[serde(default, skip_serializing_if = "Option::is_none", rename = "endIndex")]
678 pub end_index: Option<i64>,
679}
680
681#[cfg(test)]
682#[allow(clippy::unwrap_used, clippy::expect_used)]
683mod tests {
684 use super::*;
685
686 fn parse(json: serde_json::Value) -> Document {
687 serde_json::from_value(json).unwrap()
688 }
689
690 #[test]
695 fn a_missing_start_index_means_zero() {
696 let doc = parse(serde_json::json!({
697 "body": {"content": [{"endIndex": 1, "sectionBreak": {}}]},
698 }));
699 let el = &doc.body.as_ref().unwrap().content[0];
700 assert_eq!(el.start_index, None, "the key really is absent");
701 assert_eq!(el.start_index(), 0, "and the accessor reads it as zero");
702 assert_eq!(el.end_index(), 1);
703 }
704
705 #[test]
706 fn a_present_but_empty_section_break_is_still_recognised() {
707 let doc = parse(serde_json::json!({
708 "body": {"content": [{"sectionBreak": {}}]},
709 }));
710 let el = &doc.body.as_ref().unwrap().content[0];
711 assert!(el.section_break.is_some(), "present-and-empty is Some");
712 assert!(el.paragraph.is_none());
713 }
714
715 #[test]
716 fn an_absent_section_break_is_none() {
717 let doc = parse(serde_json::json!({
718 "body": {"content": [{"paragraph": {"elements": []}}]},
719 }));
720 assert!(doc.body.as_ref().unwrap().content[0]
721 .section_break
722 .is_none());
723 }
724
725 #[test]
726 fn text_run_content_defaults_to_empty() {
727 let run: TextRun = serde_json::from_value(serde_json::json!({})).unwrap();
728 assert_eq!(run.content, "");
729 }
730
731 #[test]
734 fn paragraph_text_joins_every_text_run_in_order() {
735 let para: Paragraph = serde_json::from_value(serde_json::json!({
736 "elements": [
737 {"textRun": {"content": "Hello "}},
738 {"textRun": {"content": "bold"}},
739 {"textRun": {"content": " world\n"}},
740 ],
741 }))
742 .unwrap();
743 assert_eq!(para.text(), "Hello bold world\n");
744 }
745
746 #[test]
749 fn paragraph_text_skips_a_non_text_element() {
750 let para: Paragraph = serde_json::from_value(serde_json::json!({
751 "elements": [
752 {"textRun": {"content": "a"}},
753 {"inlineObjectElement": {"inlineObjectId": "kix.1"}},
754 {"textRun": {"content": "b\n"}},
755 ],
756 }))
757 .unwrap();
758 assert_eq!(para.text(), "ab\n");
759 assert!(para.elements[1].inline_object_element.is_some());
760 }
761
762 #[test]
763 fn resolved_tabs_synthesises_one_anonymous_tab_from_a_legacy_body() {
764 let doc = parse(serde_json::json!({
765 "title": "Legacy",
766 "body": {"content": [{"endIndex": 1, "sectionBreak": {}}]},
767 }));
768 let tabs = doc.resolved_tabs();
769 assert_eq!(tabs.len(), 1);
770 assert_eq!(tabs[0].tab_id, None);
771 assert_eq!(tabs[0].title, None);
772 assert_eq!(tabs[0].nesting_level, 0);
773 assert!(tabs[0].body.is_some());
774 }
775
776 #[test]
777 fn resolved_tabs_of_a_document_with_neither_body_nor_tabs_is_empty() {
778 assert!(parse(serde_json::json!({"title": "Empty"}))
779 .resolved_tabs()
780 .is_empty());
781 }
782
783 #[test]
785 fn resolved_tabs_resolves_top_level_segments_for_a_legacy_body() {
786 let doc = parse(serde_json::json!({
787 "body": {"content": []},
788 "headers": {"h1": {"content": [{"endIndex": 1, "sectionBreak": {}}]}},
789 "footers": {"f1": {"content": []}},
790 "footnotes": {"fn1": {"content": []}},
791 }));
792 let tabs = doc.resolved_tabs();
793 assert_eq!(tabs[0].headers().len(), 1);
794 assert_eq!(tabs[0].headers()[0].segment_id, "h1");
795 assert_eq!(tabs[0].footers().len(), 1);
796 assert_eq!(tabs[0].footnotes().len(), 1);
797 }
798
799 #[test]
801 fn resolved_tabs_has_empty_segments_when_the_document_has_none() {
802 let doc = parse(serde_json::json!({"body": {"content": []}}));
803 let tabs = doc.resolved_tabs();
804 assert!(tabs[0].headers().is_empty());
805 assert!(tabs[0].footers().is_empty());
806 assert!(tabs[0].footnotes().is_empty());
807 }
808
809 #[test]
814 fn resolved_tabs_synthesises_a_tab_for_segments_with_no_body() {
815 let doc = parse(serde_json::json!({
816 "headers": {"h1": {"content": [{"endIndex": 1, "sectionBreak": {}}]}},
817 }));
818 let tabs = doc.resolved_tabs();
819 assert_eq!(tabs.len(), 1);
820 assert!(tabs[0].body.is_none());
821 assert_eq!(tabs[0].headers().len(), 1);
822 }
823
824 #[test]
827 fn resolved_tabs_resolves_tab_scoped_segments_sorted_by_segment_id() {
828 let doc = parse(serde_json::json!({
829 "tabs": [{
830 "tabProperties": {"tabId": "t.0"},
831 "documentTab": {
832 "body": {"content": []},
833 "headers": {
834 "h2": {"content": []},
835 "h1": {"content": [{"endIndex": 1, "sectionBreak": {}}]},
836 },
837 },
838 }],
839 }));
840 let tabs = doc.resolved_tabs();
841 let headers = tabs[0].headers();
842 let ids: Vec<_> = headers.iter().map(|s| s.segment_id).collect();
843 assert_eq!(ids, vec!["h1", "h2"]);
844 assert_eq!(headers[0].content.len(), 1);
845 }
846
847 #[test]
848 fn resolved_tabs_walks_child_tabs_depth_first_parent_before_children() {
849 let doc = parse(serde_json::json!({
850 "tabs": [
851 {
852 "tabProperties": {"tabId": "t.0", "title": "One", "nestingLevel": 0},
853 "documentTab": {"body": {"content": []}},
854 "childTabs": [{
855 "tabProperties": {"tabId": "t.0.a", "title": "One A", "nestingLevel": 1},
856 "documentTab": {"body": {"content": []}},
857 }],
858 },
859 {
860 "tabProperties": {"tabId": "t.1", "title": "Two", "nestingLevel": 0},
861 "documentTab": {"body": {"content": []}},
862 },
863 ],
864 }));
865 let ids: Vec<_> = doc.resolved_tabs().iter().map(|t| t.tab_id).collect();
866 assert_eq!(ids, vec![Some("t.0"), Some("t.0.a"), Some("t.1")]);
867 assert_eq!(doc.resolved_tabs()[1].nesting_level, 1);
868 }
869
870 #[test]
873 fn resolved_tabs_falls_back_to_walk_depth_when_nesting_level_is_absent() {
874 let doc = parse(serde_json::json!({
875 "tabs": [{
876 "tabProperties": {"tabId": "t.0"},
877 "childTabs": [{"tabProperties": {"tabId": "t.0.a"}}],
878 }],
879 }));
880 let tabs = doc.resolved_tabs();
881 assert_eq!(tabs[0].nesting_level, 0);
882 assert_eq!(tabs[1].nesting_level, 1);
883 }
884
885 #[test]
887 fn tabs_take_precedence_over_a_legacy_body() {
888 let doc = parse(serde_json::json!({
889 "body": {"content": [{"paragraph": {"elements": [{"textRun": {"content": "legacy\n"}}]}}]},
890 "tabs": [{
891 "tabProperties": {"tabId": "t.0"},
892 "documentTab": {"body": {"content": []}},
893 }],
894 }));
895 let tabs = doc.resolved_tabs();
896 assert_eq!(tabs.len(), 1);
897 assert_eq!(tabs[0].tab_id, Some("t.0"));
898 }
899
900 #[test]
901 fn nested_tables_parse_recursively() {
902 let doc = parse(serde_json::json!({
903 "body": {"content": [{
904 "startIndex": 1, "endIndex": 40,
905 "table": {
906 "rows": 1, "columns": 1,
907 "tableRows": [{"tableCells": [{"content": [{
908 "table": {"rows": 1, "columns": 1, "tableRows": [{"tableCells": [{
909 "content": [{"paragraph": {"elements": [
910 {"textRun": {"content": "deep\n"}}]}}],
911 }]}]},
912 }]}]}],
913 },
914 }]},
915 }));
916 let outer = doc.body.as_ref().unwrap().content[0]
917 .table
918 .as_ref()
919 .unwrap();
920 let inner = outer.table_rows[0].table_cells[0].content[0]
921 .table
922 .as_ref()
923 .unwrap();
924 let para = inner.table_rows[0].table_cells[0].content[0]
925 .paragraph
926 .as_ref()
927 .unwrap();
928 assert_eq!(para.text(), "deep\n");
929 }
930
931 #[test]
932 fn named_ranges_parse_as_a_map_keyed_by_name() {
933 let doc = parse(serde_json::json!({
934 "namedRanges": {
935 "intro": {
936 "name": "intro",
937 "namedRanges": [{
938 "namedRangeId": "nr.1", "name": "intro",
939 "ranges": [{"startIndex": 1, "endIndex": 20}],
940 }],
941 },
942 },
943 }));
944 let group = doc.named_ranges.get("intro").unwrap();
945 assert_eq!(group.name, "intro");
946 assert_eq!(group.named_ranges[0].ranges[0].end_index, Some(20));
947 }
948
949 #[test]
953 fn a_document_without_edit_access_has_no_revision_id() {
954 assert_eq!(parse(serde_json::json!({"title": "T"})).revision_id, None);
955 let with = parse(serde_json::json!({"revisionId": "rev-1"}));
956 assert_eq!(with.revision_id.as_deref(), Some("rev-1"));
957 }
958
959 #[test]
960 fn document_tolerates_unmodelled_fields() {
961 let doc = parse(serde_json::json!({
962 "documentId": "d1",
963 "title": "T",
964 "documentStyle": {"marginTop": {"magnitude": 72.0}},
965 "lists": {"kix.l1": {"listProperties": {}}},
966 "inlineObjects": {"kix.i1": {}},
967 "suggestionsViewMode": "PREVIEW_SUGGESTIONS_ACCEPTED",
968 "body": {"content": [{
969 "endIndex": 5,
970 "paragraph": {
971 "elements": [{"textRun": {
972 "content": "hi\n",
973 "textStyle": {"bold": true},
974 "suggestedInsertionIds": ["s1"],
975 }}],
976 "paragraphStyle": {"namedStyleType": "NORMAL_TEXT", "direction": "LEFT_TO_RIGHT"},
977 },
978 }]},
979 }));
980 assert_eq!(doc.document_id.as_deref(), Some("d1"));
981 let para = doc.body.as_ref().unwrap().content[0]
982 .paragraph
983 .as_ref()
984 .unwrap();
985 assert_eq!(para.text(), "hi\n");
986 assert_eq!(para.named_style_type(), Some("NORMAL_TEXT"));
987 }
988
989 #[test]
990 fn title_defaults_to_empty() {
991 assert_eq!(parse(serde_json::json!({})).title(), "");
992 assert_eq!(parse(serde_json::json!({"title": "T"})).title(), "T");
993 }
994}