Skip to main content

hwpforge_core/
document.rs

1//! The Document type with typestate pattern.
2//!
3//! [`Document<S>`] is the aggregate root of the Core DOM. It uses the
4//! **typestate pattern** to enforce document lifecycle at compile time:
5//!
6//! - [`Draft`] -- mutable, can add/remove sections
7//! - [`Validated`] -- immutable structure, safe for serialization/export
8//!
9//! The transition `Draft -> Validated` is one-way via [`Document::validate()`],
10//! which consumes the draft (move semantics prevent reuse).
11//!
12//! # Design Decisions
13//!
14//! - **Typestate, not enum** -- invalid operations are compile errors
15//!   (not runtime panics). See Appendix D in the detailed plan.
16//! - **Deserialize always to Draft** -- serialized data may be modified
17//!   externally; re-validation is mandatory.
18//! - **No `Styled` state in Phase 1** -- deferred to Phase 2 when
19//!   Blueprint (StyleRegistry) is available.
20//!
21//! # Examples
22//!
23//! ```
24//! use hwpforge_core::document::{Document, Draft, Validated};
25//! use hwpforge_core::section::Section;
26//! use hwpforge_core::paragraph::Paragraph;
27//! use hwpforge_core::run::Run;
28//! use hwpforge_core::PageSettings;
29//! use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
30//!
31//! let mut doc = Document::new();
32//! doc.add_section(Section::with_paragraphs(
33//!     vec![Paragraph::with_runs(
34//!         vec![Run::text("Hello", CharShapeIndex::new(0))],
35//!         ParaShapeIndex::new(0),
36//!     )],
37//!     PageSettings::a4(),
38//! ));
39//!
40//! let validated: Document<Validated> = doc.validate().unwrap();
41//! assert_eq!(validated.section_count(), 1);
42//! ```
43//!
44//! ```compile_fail
45//! // A validated document cannot add sections:
46//! use hwpforge_core::document::{Document, Validated};
47//! use hwpforge_core::section::Section;
48//! use hwpforge_core::PageSettings;
49//!
50//! # fn get_validated() -> Document<Validated> { todo!() }
51//! let mut validated = get_validated();
52//! validated.add_section(Section::new(PageSettings::a4()));
53//! // ERROR: no method named `add_section` found for `Document<Validated>`
54//! ```
55
56use std::marker::PhantomData;
57
58use schemars::JsonSchema;
59use serde::{Deserialize, Serialize};
60
61use crate::error::CoreResult;
62use crate::metadata::Metadata;
63use crate::paragraph::Paragraph;
64use crate::section::Section;
65use crate::validate::validate_sections;
66
67/// Marker type: the document is a mutable draft.
68///
69/// A `Document<Draft>` can be modified (add sections, set metadata)
70/// and then validated via [`Document::validate()`].
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Draft;
73
74/// Marker type: the document has passed structural validation.
75///
76/// A `Document<Validated>` is guaranteed to have:
77/// - At least 1 section
78/// - Every section has at least 1 paragraph
79/// - Every paragraph has at least 1 run
80/// - All table/control structural invariants hold
81///
82/// The only way to obtain a `Document<Validated>` is through
83/// [`Document<Draft>::validate()`].
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Validated;
86
87/// The document aggregate root with compile-time state tracking.
88///
89/// The generic parameter `S` determines which operations are available:
90///
91/// | State | Mutable | Serializable | Exportable |
92/// |-------|---------|-------------|-----------|
93/// | [`Draft`] | Yes | Yes | No (must validate first) |
94/// | [`Validated`] | No | Yes | Yes |
95///
96/// # Typestate Safety
97///
98/// The `_state` field is private and zero-sized. There is no way to
99/// construct a `Document<Validated>` except through `validate()`.
100///
101/// # Examples
102///
103/// ```
104/// use hwpforge_core::document::Document;
105/// use hwpforge_core::Metadata;
106///
107/// let doc = Document::with_metadata(Metadata::new().with_title("Report"));
108/// assert!(doc.is_empty());
109/// ```
110pub struct Document<S = Draft> {
111    sections: Vec<Section>,
112    metadata: Metadata,
113    _state: PhantomData<S>,
114}
115
116// ---------------------------------------------------------------------------
117// Shared methods (any state)
118// ---------------------------------------------------------------------------
119
120impl<S> Document<S> {
121    /// Returns a slice of all sections.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use hwpforge_core::document::Document;
127    ///
128    /// let doc = Document::new();
129    /// assert!(doc.sections().is_empty());
130    /// ```
131    pub fn sections(&self) -> &[Section] {
132        &self.sections
133    }
134
135    /// Returns a reference to the document metadata.
136    pub fn metadata(&self) -> &Metadata {
137        &self.metadata
138    }
139
140    /// Returns the number of sections.
141    pub fn section_count(&self) -> usize {
142        self.sections.len()
143    }
144
145    /// Returns `true` if the document has no sections.
146    pub fn is_empty(&self) -> bool {
147        self.sections.is_empty()
148    }
149}
150
151// ---------------------------------------------------------------------------
152// Draft-only methods
153// ---------------------------------------------------------------------------
154
155impl Document<Draft> {
156    /// Creates a new empty draft document with default metadata.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use hwpforge_core::document::Document;
162    ///
163    /// let doc = Document::new();
164    /// assert!(doc.is_empty());
165    /// ```
166    pub fn new() -> Self {
167        Self { sections: Vec::new(), metadata: Metadata::default(), _state: PhantomData }
168    }
169
170    /// Creates a new draft document with the given metadata.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use hwpforge_core::document::Document;
176    /// use hwpforge_core::Metadata;
177    ///
178    /// let doc = Document::with_metadata(Metadata::new().with_title("Test"));
179    /// assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
180    /// ```
181    pub fn with_metadata(metadata: Metadata) -> Self {
182        Self { sections: Vec::new(), metadata, _state: PhantomData }
183    }
184
185    /// 문서의 모든 문단(전 섹션 본문·머리말·꼬리말·바탕쪽 + 표 셀·캡션·
186    /// 글상자·각주/미주·메모 등 중첩 문단 전부)을 문서 순서로 방문한다.
187    ///
188    /// 캐시 정규화([`Self::strip_layout_caches`]) 등 전 문단 일괄 변환의
189    /// 진입점이다. `Draft` 전용 — `Validated` 는 typestate 상 불변이므로
190    /// 비교가 필요하면 검증 전 사본에서 수행한다.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use hwpforge_core::document::Document;
196    /// use hwpforge_core::page::PageSettings;
197    /// use hwpforge_core::paragraph::Paragraph;
198    /// use hwpforge_core::section::Section;
199    /// use hwpforge_foundation::ParaShapeIndex;
200    ///
201    /// let mut doc = Document::new();
202    /// doc.add_section(Section::with_paragraphs(
203    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
204    ///     PageSettings::a4(),
205    /// ));
206    /// let mut count = 0;
207    /// doc.for_each_paragraph_mut(|_| count += 1);
208    /// assert_eq!(count, 1);
209    /// ```
210    pub fn for_each_paragraph_mut<F: FnMut(&mut Paragraph)>(&mut self, mut f: F) {
211        for section in &mut self.sections {
212            section.walk_paragraphs_mut(&mut f);
213        }
214    }
215
216    /// 모든 문단의 줄 조판 캐시([`Paragraph::layout_cache`])를 제거한다.
217    ///
218    /// 문서 동등성 비교(admission/golden)의 정규화 단계: 네이티브 입력은
219    /// 캐시를 보유하고 우리 재인코드 산출물은 미보유가 정상이므로, 비교
220    /// 전 양쪽 사본에서 이 함수를 호출해 캐시 차이를 제거한다.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use hwpforge_core::document::Document;
226    /// use hwpforge_core::layout::LayoutCache;
227    /// use hwpforge_core::page::PageSettings;
228    /// use hwpforge_core::paragraph::Paragraph;
229    /// use hwpforge_core::section::Section;
230    /// use hwpforge_foundation::ParaShapeIndex;
231    ///
232    /// let mut para = Paragraph::new(ParaShapeIndex::new(0));
233    /// para.layout_cache = Some(LayoutCache::default());
234    /// let mut doc = Document::new();
235    /// doc.add_section(Section::with_paragraphs(vec![para], PageSettings::a4()));
236    /// doc.strip_layout_caches();
237    /// assert!(doc.sections()[0].paragraphs[0].layout_cache.is_none());
238    /// ```
239    pub fn strip_layout_caches(&mut self) {
240        self.for_each_paragraph_mut(|p| {
241            p.layout_cache = None;
242            // 표 수준 decode-only 캐시도 함께 제거 — 방문 문단의 직계 run 만
243            // 보면 충분하다 (중첩 표의 host 는 셀 문단이고, walker 가 셀
244            // 문단도 방문한다).
245            for run in &mut p.runs {
246                if let crate::run::RunContent::Table(table) = &mut run.content {
247                    table.layout_cache = None;
248                }
249            }
250        });
251    }
252
253    /// Appends a section to the draft document.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use hwpforge_core::document::Document;
259    /// use hwpforge_core::section::Section;
260    /// use hwpforge_core::PageSettings;
261    ///
262    /// let mut doc = Document::new();
263    /// doc.add_section(Section::new(PageSettings::a4()));
264    /// assert_eq!(doc.section_count(), 1);
265    /// ```
266    pub fn add_section(&mut self, section: Section) {
267        self.sections.push(section);
268    }
269
270    /// Sets the document metadata.
271    pub fn set_metadata(&mut self, metadata: Metadata) {
272        self.metadata = metadata;
273    }
274
275    /// Returns a mutable reference to the metadata.
276    pub fn metadata_mut(&mut self) -> &mut Metadata {
277        &mut self.metadata
278    }
279
280    /// Returns a mutable slice of sections.
281    pub fn sections_mut(&mut self) -> &mut [Section] {
282        &mut self.sections
283    }
284
285    /// Validates the document structure and transitions to `Validated`.
286    ///
287    /// Consumes `self` (move semantics). On success, returns a
288    /// `Document<Validated>`. On failure, returns a `CoreError`.
289    ///
290    /// # Errors
291    ///
292    /// Returns [`CoreError::Validation`](crate::error::CoreError::Validation) if the document violates any
293    /// structural invariant (empty sections, empty paragraphs, etc.).
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use hwpforge_core::document::Document;
299    /// use hwpforge_core::section::Section;
300    /// use hwpforge_core::paragraph::Paragraph;
301    /// use hwpforge_core::run::Run;
302    /// use hwpforge_core::PageSettings;
303    /// use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
304    ///
305    /// let mut doc = Document::new();
306    /// doc.add_section(Section::with_paragraphs(
307    ///     vec![Paragraph::with_runs(
308    ///         vec![Run::text("Hello", CharShapeIndex::new(0))],
309    ///         ParaShapeIndex::new(0),
310    ///     )],
311    ///     PageSettings::a4(),
312    /// ));
313    ///
314    /// let validated = doc.validate().unwrap();
315    /// assert_eq!(validated.section_count(), 1);
316    /// ```
317    ///
318    /// ```
319    /// use hwpforge_core::document::Document;
320    ///
321    /// let doc = Document::new(); // empty
322    /// assert!(doc.validate().is_err());
323    /// ```
324    pub fn validate(self) -> CoreResult<Document<Validated>> {
325        validate_sections(&self.sections)?;
326        Ok(Document { sections: self.sections, metadata: self.metadata, _state: PhantomData })
327    }
328}
329
330impl Default for Document<Draft> {
331    fn default() -> Self {
332        Self::new()
333    }
334}
335
336// ---------------------------------------------------------------------------
337// Manual trait impls (avoid T: Trait bounds on phantom type S)
338// ---------------------------------------------------------------------------
339
340impl<S> std::fmt::Debug for Document<S> {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        f.debug_struct("Document")
343            .field("sections", &self.sections)
344            .field("metadata", &self.metadata)
345            .finish()
346    }
347}
348
349impl<S> Clone for Document<S> {
350    fn clone(&self) -> Self {
351        Self {
352            sections: self.sections.clone(),
353            metadata: self.metadata.clone(),
354            _state: PhantomData,
355        }
356    }
357}
358
359impl<S> PartialEq for Document<S> {
360    fn eq(&self, other: &Self) -> bool {
361        self.sections == other.sections && self.metadata == other.metadata
362    }
363}
364
365impl<S> Eq for Document<S> {}
366
367impl<S> std::fmt::Display for Document<S> {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        write!(f, "Document({} sections)", self.sections.len())
370    }
371}
372
373// ---------------------------------------------------------------------------
374// Serde: serialize any state, deserialize only to Draft
375// ---------------------------------------------------------------------------
376
377impl<S> Serialize for Document<S> {
378    fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
379        use serde::ser::SerializeStruct;
380        let mut state = serializer.serialize_struct("Document", 2)?;
381        state.serialize_field("sections", &self.sections)?;
382        state.serialize_field("metadata", &self.metadata)?;
383        state.end()
384    }
385}
386
387impl<'de> Deserialize<'de> for Document<Draft> {
388    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
389        #[derive(Deserialize)]
390        struct DocumentData {
391            sections: Vec<Section>,
392            metadata: Metadata,
393        }
394
395        let data = DocumentData::deserialize(deserializer)?;
396        Ok(Document { sections: data.sections, metadata: data.metadata, _state: PhantomData })
397    }
398}
399
400// ---------------------------------------------------------------------------
401// JsonSchema: hide PhantomData
402// ---------------------------------------------------------------------------
403
404impl<S> JsonSchema for Document<S> {
405    fn schema_name() -> std::borrow::Cow<'static, str> {
406        "Document".into()
407    }
408
409    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
410        schemars::json_schema!({
411            "type": "object",
412            "properties": {
413                "sections": gen.subschema_for::<Vec<Section>>(),
414                "metadata": gen.subschema_for::<crate::metadata::Metadata>(),
415            },
416            "required": ["sections", "metadata"]
417        })
418    }
419}
420
421// ---------------------------------------------------------------------------
422// Send + Sync verification
423// ---------------------------------------------------------------------------
424
425const _: () = {
426    #[allow(dead_code)]
427    fn assert_send<T: Send>() {}
428    #[allow(dead_code)]
429    fn assert_sync<T: Sync>() {}
430    #[allow(dead_code)]
431    fn verify() {
432        assert_send::<Document<Draft>>();
433        assert_sync::<Document<Draft>>();
434        assert_send::<Document<Validated>>();
435        assert_sync::<Document<Validated>>();
436    }
437};
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::error::CoreError;
443    use crate::page::PageSettings;
444    use crate::paragraph::Paragraph;
445    use crate::run::Run;
446    use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
447
448    fn valid_section() -> Section {
449        Section::with_paragraphs(
450            vec![Paragraph::with_runs(
451                vec![Run::text("Hello", CharShapeIndex::new(0))],
452                ParaShapeIndex::new(0),
453            )],
454            PageSettings::a4(),
455        )
456    }
457
458    // === Construction ===
459
460    #[test]
461    fn new_creates_empty_draft() {
462        let doc = Document::new();
463        assert!(doc.is_empty());
464        assert_eq!(doc.section_count(), 0);
465        assert!(doc.metadata().title.is_none());
466    }
467
468    #[test]
469    fn with_metadata() {
470        let meta = Metadata { title: Some("Test".to_string()), ..Metadata::default() };
471        let doc = Document::with_metadata(meta);
472        assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
473    }
474
475    #[test]
476    fn default_is_new() {
477        let a = Document::new();
478        let b = Document::default();
479        assert_eq!(a, b);
480    }
481
482    // === Draft mutations ===
483
484    #[test]
485    fn add_section() {
486        let mut doc = Document::new();
487        doc.add_section(valid_section());
488        assert_eq!(doc.section_count(), 1);
489        assert!(!doc.is_empty());
490    }
491
492    #[test]
493    fn add_multiple_sections() {
494        let mut doc = Document::new();
495        doc.add_section(valid_section());
496        doc.add_section(valid_section());
497        doc.add_section(valid_section());
498        assert_eq!(doc.section_count(), 3);
499    }
500
501    #[test]
502    fn set_metadata() {
503        let mut doc = Document::new();
504        doc.set_metadata(Metadata { title: Some("New".to_string()), ..Metadata::default() });
505        assert_eq!(doc.metadata().title.as_deref(), Some("New"));
506    }
507
508    #[test]
509    fn metadata_mut() {
510        let mut doc = Document::new();
511        doc.metadata_mut().title = Some("Mutated".to_string());
512        assert_eq!(doc.metadata().title.as_deref(), Some("Mutated"));
513    }
514
515    #[test]
516    fn sections_mut() {
517        let mut doc = Document::new();
518        doc.add_section(valid_section());
519        doc.add_section(valid_section());
520        assert_eq!(doc.sections_mut().len(), 2);
521    }
522
523    // === Validation (Draft -> Validated) ===
524
525    #[test]
526    fn validate_success() {
527        let mut doc = Document::new();
528        doc.add_section(valid_section());
529        let validated = doc.validate().unwrap();
530        assert_eq!(validated.section_count(), 1);
531    }
532
533    #[test]
534    fn validate_empty_document_fails() {
535        let doc = Document::new();
536        let err = doc.validate().unwrap_err();
537        assert!(matches!(err, CoreError::Validation(_)));
538    }
539
540    #[test]
541    fn validate_empty_section_fails() {
542        let mut doc = Document::new();
543        doc.add_section(Section::new(PageSettings::a4()));
544        assert!(doc.validate().is_err());
545    }
546
547    #[test]
548    fn validate_consumes_draft() {
549        let mut doc = Document::new();
550        doc.add_section(valid_section());
551        let _validated = doc.validate().unwrap();
552        // doc is moved -- attempting to use it would be a compile error
553    }
554
555    // === Validated state ===
556
557    #[test]
558    fn validated_has_read_methods() {
559        let mut doc = Document::new();
560        doc.add_section(valid_section());
561        let validated = doc.validate().unwrap();
562
563        assert_eq!(validated.section_count(), 1);
564        assert!(!validated.is_empty());
565        assert_eq!(validated.sections().len(), 1);
566        assert!(validated.metadata().title.is_none());
567    }
568
569    // === Display ===
570
571    #[test]
572    fn display_draft() {
573        let doc = Document::new();
574        assert_eq!(doc.to_string(), "Document(0 sections)");
575    }
576
577    #[test]
578    fn display_validated() {
579        let mut doc = Document::new();
580        doc.add_section(valid_section());
581        let validated = doc.validate().unwrap();
582        assert_eq!(validated.to_string(), "Document(1 sections)");
583    }
584
585    // === Equality ===
586
587    #[test]
588    fn equality_draft() {
589        let mut a = Document::new();
590        a.add_section(valid_section());
591        let mut b = Document::new();
592        b.add_section(valid_section());
593        assert_eq!(a, b);
594    }
595
596    #[test]
597    fn equality_validated() {
598        let mut a = Document::new();
599        a.add_section(valid_section());
600        let mut b = Document::new();
601        b.add_section(valid_section());
602        let va = a.validate().unwrap();
603        let vb = b.validate().unwrap();
604        assert_eq!(va, vb);
605    }
606
607    // === Clone ===
608
609    #[test]
610    fn clone_draft() {
611        let mut doc = Document::new();
612        doc.add_section(valid_section());
613        let cloned = doc.clone();
614        assert_eq!(doc, cloned);
615    }
616
617    #[test]
618    fn clone_validated() {
619        let mut doc = Document::new();
620        doc.add_section(valid_section());
621        let validated = doc.validate().unwrap();
622        let cloned = validated.clone();
623        assert_eq!(validated, cloned);
624    }
625
626    // === Serde ===
627
628    #[test]
629    fn serde_roundtrip_draft() {
630        let mut doc = Document::new();
631        doc.add_section(valid_section());
632        doc.set_metadata(Metadata { title: Some("Test".to_string()), ..Metadata::default() });
633
634        let json = serde_json::to_string(&doc).unwrap();
635        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
636        assert_eq!(doc, back);
637    }
638
639    #[test]
640    fn serde_roundtrip_validated_deserializes_to_draft() {
641        let mut doc = Document::new();
642        doc.add_section(valid_section());
643        let validated = doc.validate().unwrap();
644
645        let json = serde_json::to_string(&validated).unwrap();
646        // Deserialize always produces Draft
647        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
648        // Must re-validate
649        let re_validated = back.validate().unwrap();
650        assert_eq!(validated, re_validated);
651    }
652
653    #[test]
654    fn serde_empty_document() {
655        let doc = Document::new();
656        let json = serde_json::to_string(&doc).unwrap();
657        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
658        assert_eq!(doc, back);
659    }
660
661    // === Complex document ===
662
663    #[test]
664    fn complex_document_roundtrip() {
665        use crate::control::Control;
666        use crate::image::{Image, ImageFormat};
667        use crate::table::{Table, TableCell, TableRow};
668        use hwpforge_foundation::HwpUnit;
669
670        let cell = TableCell::new(
671            vec![Paragraph::with_runs(
672                vec![Run::text("cell", CharShapeIndex::new(0))],
673                ParaShapeIndex::new(0),
674            )],
675            HwpUnit::from_mm(50.0).unwrap(),
676        );
677        let table = Table::new(vec![TableRow::new(vec![cell])]);
678
679        let link = Control::Hyperlink {
680            text: "click".to_string(),
681            url: "https://example.com".to_string(),
682        };
683
684        let img = Image::new(
685            "test.png",
686            HwpUnit::from_mm(10.0).unwrap(),
687            HwpUnit::from_mm(10.0).unwrap(),
688            ImageFormat::Png,
689        );
690
691        let section = Section::with_paragraphs(
692            vec![
693                Paragraph::with_runs(
694                    vec![
695                        Run::text("Hello ", CharShapeIndex::new(0)),
696                        Run::text("world", CharShapeIndex::new(1)),
697                    ],
698                    ParaShapeIndex::new(0),
699                ),
700                Paragraph::with_runs(
701                    vec![Run::table(table, CharShapeIndex::new(0))],
702                    ParaShapeIndex::new(1),
703                ),
704                Paragraph::with_runs(
705                    vec![Run::control(link, CharShapeIndex::new(0))],
706                    ParaShapeIndex::new(0),
707                ),
708                Paragraph::with_runs(
709                    vec![Run::image(img, CharShapeIndex::new(0))],
710                    ParaShapeIndex::new(0),
711                ),
712            ],
713            PageSettings::a4(),
714        );
715
716        let mut doc = Document::with_metadata(Metadata {
717            title: Some("Complex Doc".to_string()),
718            author: Some("Author".to_string()),
719            keywords: vec!["test".to_string()],
720            ..Metadata::default()
721        });
722        doc.add_section(section);
723
724        let validated = doc.validate().unwrap();
725        let json = serde_json::to_string_pretty(&validated).unwrap();
726        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
727        let re_validated = back.validate().unwrap();
728        assert_eq!(validated, re_validated);
729    }
730
731    // === Debug ===
732
733    #[test]
734    fn debug_output() {
735        let doc = Document::new();
736        let s = format!("{doc:?}");
737        assert!(s.contains("Document"), "debug: {s}");
738        assert!(s.contains("sections"), "debug: {s}");
739    }
740}