acdc_parser/model/
mod.rs

1//! The data models for the `AsciiDoc` document.
2use std::{fmt::Display, str::FromStr, string::ToString};
3
4use serde::{
5    Deserialize, Serialize,
6    de::{self, Deserializer},
7    ser::{SerializeMap, Serializer},
8};
9
10mod admonition;
11mod anchor;
12mod attributes;
13mod inlines;
14mod lists;
15mod location;
16mod media;
17mod metadata;
18mod section;
19mod substitution;
20mod tables;
21mod title;
22
23pub use admonition::{Admonition, AdmonitionVariant};
24pub use anchor::{Anchor, TocEntry};
25pub use attributes::{AttributeName, AttributeValue, DocumentAttributes, ElementAttributes};
26pub use inlines::*;
27pub use lists::{
28    CalloutList, DescriptionList, DescriptionListItem, ListItem, ListItemCheckedStatus, ListLevel,
29    OrderedList, UnorderedList,
30};
31pub use location::*;
32pub use media::{Audio, Image, Source, Video};
33pub use metadata::{BlockMetadata, Role};
34pub use section::*;
35pub use substitution::*;
36pub use tables::{
37    ColumnFormat, ColumnStyle, ColumnWidth, HorizontalAlignment, Table, TableColumn, TableRow,
38    VerticalAlignment,
39};
40pub use title::{Subtitle, Title};
41
42/// A `Document` represents the root of an `AsciiDoc` document.
43#[derive(Default, Debug, PartialEq, Deserialize)]
44#[non_exhaustive]
45pub struct Document {
46    pub(crate) name: String,
47    pub(crate) r#type: String,
48    #[serde(default)]
49    pub header: Option<Header>,
50    #[serde(default, skip_serializing_if = "DocumentAttributes::is_empty")]
51    pub attributes: DocumentAttributes,
52    #[serde(default)]
53    pub blocks: Vec<Block>,
54    #[serde(skip)]
55    pub footnotes: Vec<Footnote>,
56    #[serde(skip)]
57    pub toc_entries: Vec<TocEntry>,
58    pub location: Location,
59}
60
61/// A `Header` represents the header of a document.
62///
63/// The header contains the title, subtitle, authors, and optional metadata
64/// (such as ID and roles) that can be applied to the document title.
65#[derive(Debug, PartialEq, Serialize, Deserialize)]
66#[non_exhaustive]
67pub struct Header {
68    #[serde(default, skip_serializing_if = "BlockMetadata::is_default")]
69    pub metadata: BlockMetadata,
70    #[serde(default, skip_serializing_if = "Title::is_empty")]
71    pub title: Title,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub subtitle: Option<Subtitle>,
74    #[serde(default, skip_serializing_if = "Vec::is_empty")]
75    pub authors: Vec<Author>,
76    pub location: Location,
77}
78
79/// An `Author` represents the author of a document.
80#[derive(Debug, PartialEq, Serialize, Deserialize)]
81#[non_exhaustive]
82pub struct Author {
83    #[serde(rename = "firstname")]
84    pub first_name: String,
85    #[serde(
86        default,
87        skip_serializing_if = "Option::is_none",
88        rename = "middlename"
89    )]
90    pub middle_name: Option<String>,
91    #[serde(rename = "lastname")]
92    pub last_name: String,
93    pub initials: String,
94    #[serde(default, skip_serializing_if = "Option::is_none", rename = "address")]
95    pub email: Option<String>,
96}
97
98impl Header {
99    /// Create a new header with the given title and location.
100    #[must_use]
101    pub fn new(title: Title, location: Location) -> Self {
102        Self {
103            metadata: BlockMetadata::default(),
104            title,
105            subtitle: None,
106            authors: Vec::new(),
107            location,
108        }
109    }
110
111    /// Set the metadata.
112    #[must_use]
113    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
114        self.metadata = metadata;
115        self
116    }
117
118    /// Set the subtitle.
119    #[must_use]
120    pub fn with_subtitle(mut self, subtitle: Subtitle) -> Self {
121        self.subtitle = Some(subtitle);
122        self
123    }
124
125    /// Set the authors.
126    #[must_use]
127    pub fn with_authors(mut self, authors: Vec<Author>) -> Self {
128        self.authors = authors;
129        self
130    }
131}
132
133impl Author {
134    /// Create a new author with the given names and initials.
135    #[must_use]
136    pub fn new(first_name: &str, middle_name: Option<&str>, last_name: Option<&str>) -> Self {
137        let initials = Self::generate_initials(first_name, middle_name, last_name);
138        let last_name = last_name.unwrap_or_default().to_string();
139        Self {
140            first_name: first_name.to_string(),
141            middle_name: middle_name.map(ToString::to_string),
142            last_name,
143            initials,
144            email: None,
145        }
146    }
147
148    /// Set the email address.
149    #[must_use]
150    pub fn with_email(mut self, email: String) -> Self {
151        self.email = Some(email);
152        self
153    }
154
155    /// Generate initials from first, optional middle, and last name parts
156    fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
157        let first_initial = first.chars().next().unwrap_or_default().to_string();
158        let middle_initial = middle
159            .map(|m| m.chars().next().unwrap_or_default().to_string())
160            .unwrap_or_default();
161        let last_initial = last
162            .map(|m| m.chars().next().unwrap_or_default().to_string())
163            .unwrap_or_default();
164        first_initial + &middle_initial + &last_initial
165    }
166}
167
168/// A single-line comment in a document.
169///
170/// Line comments begin with `//` and continue to end of line.
171/// They act as block boundaries but produce no output.
172#[derive(Clone, Debug, PartialEq)]
173#[non_exhaustive]
174pub struct Comment {
175    pub content: String,
176    pub location: Location,
177}
178
179/// A `Block` represents a block in a document.
180///
181/// A block is a structural element in a document that can contain other blocks.
182#[non_exhaustive]
183#[derive(Clone, Debug, PartialEq, Serialize)]
184#[serde(untagged)]
185pub enum Block {
186    TableOfContents(TableOfContents),
187    // TODO(nlopes): we shouldn't have an admonition type here, instead it should be
188    // picked up from the style attribute from the block metadata.
189    //
190    // The main one that would need changing is the Paragraph and the Delimited Example
191    // blocks, where we currently use this but don't need to.
192    Admonition(Admonition),
193    DiscreteHeader(DiscreteHeader),
194    DocumentAttribute(DocumentAttribute),
195    ThematicBreak(ThematicBreak),
196    PageBreak(PageBreak),
197    UnorderedList(UnorderedList),
198    OrderedList(OrderedList),
199    CalloutList(CalloutList),
200    DescriptionList(DescriptionList),
201    Section(Section),
202    DelimitedBlock(DelimitedBlock),
203    Paragraph(Paragraph),
204    Image(Image),
205    Audio(Audio),
206    Video(Video),
207    Comment(Comment),
208}
209
210impl Locateable for Block {
211    fn location(&self) -> &Location {
212        match self {
213            Block::Section(s) => &s.location,
214            Block::Paragraph(p) => &p.location,
215            Block::UnorderedList(l) => &l.location,
216            Block::OrderedList(l) => &l.location,
217            Block::DescriptionList(l) => &l.location,
218            Block::CalloutList(l) => &l.location,
219            Block::DelimitedBlock(d) => &d.location,
220            Block::Admonition(a) => &a.location,
221            Block::TableOfContents(t) => &t.location,
222            Block::DiscreteHeader(h) => &h.location,
223            Block::DocumentAttribute(a) => &a.location,
224            Block::ThematicBreak(tb) => &tb.location,
225            Block::PageBreak(pb) => &pb.location,
226            Block::Image(i) => &i.location,
227            Block::Audio(a) => &a.location,
228            Block::Video(v) => &v.location,
229            Block::Comment(c) => &c.location,
230        }
231    }
232}
233
234/// A `DocumentAttribute` represents a document attribute in a document.
235///
236/// A document attribute is a key-value pair that can be used to set metadata in a
237/// document.
238#[derive(Clone, Debug, PartialEq)]
239#[non_exhaustive]
240pub struct DocumentAttribute {
241    pub name: AttributeName,
242    pub value: AttributeValue,
243    pub location: Location,
244}
245
246impl Serialize for DocumentAttribute {
247    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248    where
249        S: Serializer,
250    {
251        let mut state = serializer.serialize_map(None)?;
252        state.serialize_entry("name", &self.name)?;
253        state.serialize_entry("type", "attribute")?;
254        state.serialize_entry("value", &self.value)?;
255        state.serialize_entry("location", &self.location)?;
256        state.end()
257    }
258}
259
260/// A `DiscreteHeader` represents a discrete header in a document.
261///
262/// Discrete headings are useful for making headings inside of other blocks, like a
263/// sidebar.
264#[derive(Clone, Debug, PartialEq)]
265#[non_exhaustive]
266pub struct DiscreteHeader {
267    pub metadata: BlockMetadata,
268    pub title: Title,
269    pub level: u8,
270    pub location: Location,
271}
272
273/// A `ThematicBreak` represents a thematic break in a document.
274#[derive(Clone, Default, Debug, PartialEq)]
275#[non_exhaustive]
276pub struct ThematicBreak {
277    pub anchors: Vec<Anchor>,
278    pub title: Title,
279    pub location: Location,
280}
281
282impl Serialize for ThematicBreak {
283    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
284    where
285        S: Serializer,
286    {
287        let mut state = serializer.serialize_map(None)?;
288        state.serialize_entry("name", "break")?;
289        state.serialize_entry("type", "block")?;
290        state.serialize_entry("variant", "thematic")?;
291        if !self.anchors.is_empty() {
292            state.serialize_entry("anchors", &self.anchors)?;
293        }
294        if !self.title.is_empty() {
295            state.serialize_entry("title", &self.title)?;
296        }
297        state.serialize_entry("location", &self.location)?;
298        state.end()
299    }
300}
301
302/// A `PageBreak` represents a page break in a document.
303#[derive(Clone, Debug, PartialEq)]
304#[non_exhaustive]
305pub struct PageBreak {
306    pub title: Title,
307    pub metadata: BlockMetadata,
308    pub location: Location,
309}
310
311impl Serialize for PageBreak {
312    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
313    where
314        S: Serializer,
315    {
316        let mut state = serializer.serialize_map(None)?;
317        state.serialize_entry("name", "break")?;
318        state.serialize_entry("type", "block")?;
319        state.serialize_entry("variant", "page")?;
320        if !self.title.is_empty() {
321            state.serialize_entry("title", &self.title)?;
322        }
323        if !self.metadata.is_default() {
324            state.serialize_entry("metadata", &self.metadata)?;
325        }
326        state.serialize_entry("location", &self.location)?;
327        state.end()
328    }
329}
330
331impl Serialize for Comment {
332    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
333    where
334        S: Serializer,
335    {
336        let mut state = serializer.serialize_map(None)?;
337        state.serialize_entry("name", "comment")?;
338        state.serialize_entry("type", "block")?;
339        if !self.content.is_empty() {
340            state.serialize_entry("content", &self.content)?;
341        }
342        state.serialize_entry("location", &self.location)?;
343        state.end()
344    }
345}
346
347/// A `TableOfContents` represents a table of contents block.
348#[derive(Clone, Debug, PartialEq)]
349#[non_exhaustive]
350pub struct TableOfContents {
351    pub metadata: BlockMetadata,
352    pub location: Location,
353}
354
355impl Serialize for TableOfContents {
356    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
357    where
358        S: Serializer,
359    {
360        let mut state = serializer.serialize_map(None)?;
361        state.serialize_entry("name", "toc")?;
362        state.serialize_entry("type", "block")?;
363        if !self.metadata.is_default() {
364            state.serialize_entry("metadata", &self.metadata)?;
365        }
366        state.serialize_entry("location", &self.location)?;
367        state.end()
368    }
369}
370
371/// A `Paragraph` represents a paragraph in a document.
372#[derive(Clone, Debug, PartialEq)]
373#[non_exhaustive]
374pub struct Paragraph {
375    pub metadata: BlockMetadata,
376    pub title: Title,
377    pub content: Vec<InlineNode>,
378    pub location: Location,
379}
380
381impl Paragraph {
382    /// Create a new paragraph with the given content and location.
383    #[must_use]
384    pub fn new(content: Vec<InlineNode>, location: Location) -> Self {
385        Self {
386            metadata: BlockMetadata::default(),
387            title: Title::default(),
388            content,
389            location,
390        }
391    }
392
393    /// Set the metadata.
394    #[must_use]
395    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
396        self.metadata = metadata;
397        self
398    }
399
400    /// Set the title.
401    #[must_use]
402    pub fn with_title(mut self, title: Title) -> Self {
403        self.title = title;
404        self
405    }
406}
407
408/// A `DelimitedBlock` represents a delimited block in a document.
409#[derive(Clone, Debug, PartialEq)]
410#[non_exhaustive]
411pub struct DelimitedBlock {
412    pub metadata: BlockMetadata,
413    pub inner: DelimitedBlockType,
414    pub delimiter: String,
415    pub title: Title,
416    pub location: Location,
417}
418
419impl DelimitedBlock {
420    /// Create a new delimited block.
421    #[must_use]
422    pub fn new(inner: DelimitedBlockType, delimiter: String, location: Location) -> Self {
423        Self {
424            metadata: BlockMetadata::default(),
425            inner,
426            delimiter,
427            title: Title::default(),
428            location,
429        }
430    }
431
432    /// Set the metadata.
433    #[must_use]
434    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
435        self.metadata = metadata;
436        self
437    }
438
439    /// Set the title.
440    #[must_use]
441    pub fn with_title(mut self, title: Title) -> Self {
442        self.title = title;
443        self
444    }
445}
446
447/// Notation type for mathematical expressions.
448#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
449#[serde(rename_all = "lowercase")]
450pub enum StemNotation {
451    Latexmath,
452    Asciimath,
453}
454
455impl Display for StemNotation {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        match self {
458            StemNotation::Latexmath => write!(f, "latexmath"),
459            StemNotation::Asciimath => write!(f, "asciimath"),
460        }
461    }
462}
463
464impl FromStr for StemNotation {
465    type Err = String;
466
467    fn from_str(s: &str) -> Result<Self, Self::Err> {
468        match s {
469            "latexmath" => Ok(Self::Latexmath),
470            "asciimath" => Ok(Self::Asciimath),
471            _ => Err(format!("unknown stem notation: {s}")),
472        }
473    }
474}
475
476/// Content of a stem block with math notation.
477#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
478#[non_exhaustive]
479pub struct StemContent {
480    pub content: String,
481    pub notation: StemNotation,
482}
483
484impl StemContent {
485    /// Create a new stem content with the given content and notation.
486    #[must_use]
487    pub fn new(content: String, notation: StemNotation) -> Self {
488        Self { content, notation }
489    }
490}
491
492/// A `DelimitedBlockType` represents the type of a delimited block in a document.
493#[non_exhaustive]
494#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
495#[serde(untagged)]
496pub enum DelimitedBlockType {
497    DelimitedComment(Vec<InlineNode>),
498    DelimitedExample(Vec<Block>),
499    DelimitedListing(Vec<InlineNode>),
500    DelimitedLiteral(Vec<InlineNode>),
501    DelimitedOpen(Vec<Block>),
502    DelimitedSidebar(Vec<Block>),
503    DelimitedTable(Table),
504    DelimitedPass(Vec<InlineNode>),
505    DelimitedQuote(Vec<Block>),
506    DelimitedVerse(Vec<InlineNode>),
507    DelimitedStem(StemContent),
508}
509
510impl DelimitedBlockType {
511    fn name(&self) -> &'static str {
512        match self {
513            DelimitedBlockType::DelimitedComment(_) => "comment",
514            DelimitedBlockType::DelimitedExample(_) => "example",
515            DelimitedBlockType::DelimitedListing(_) => "listing",
516            DelimitedBlockType::DelimitedLiteral(_) => "literal",
517            DelimitedBlockType::DelimitedOpen(_) => "open",
518            DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
519            DelimitedBlockType::DelimitedTable(_) => "table",
520            DelimitedBlockType::DelimitedPass(_) => "pass",
521            DelimitedBlockType::DelimitedQuote(_) => "quote",
522            DelimitedBlockType::DelimitedVerse(_) => "verse",
523            DelimitedBlockType::DelimitedStem(_) => "stem",
524        }
525    }
526}
527
528impl Serialize for Document {
529    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
530    where
531        S: Serializer,
532    {
533        let mut state = serializer.serialize_map(None)?;
534        state.serialize_entry("name", "document")?;
535        state.serialize_entry("type", "block")?;
536        if let Some(header) = &self.header {
537            state.serialize_entry("header", header)?;
538            // We serialize the attributes even if they're empty because that's what the
539            // TCK expects (odd but true)
540            state.serialize_entry("attributes", &self.attributes)?;
541        } else if !self.attributes.is_empty() {
542            state.serialize_entry("attributes", &self.attributes)?;
543        }
544        if !self.blocks.is_empty() {
545            state.serialize_entry("blocks", &self.blocks)?;
546        }
547        state.serialize_entry("location", &self.location)?;
548        state.end()
549    }
550}
551
552impl Serialize for DelimitedBlock {
553    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
554    where
555        S: Serializer,
556    {
557        let mut state = serializer.serialize_map(None)?;
558        state.serialize_entry("name", self.inner.name())?;
559        state.serialize_entry("type", "block")?;
560        state.serialize_entry("form", "delimited")?;
561        state.serialize_entry("delimiter", &self.delimiter)?;
562        if !self.metadata.is_default() {
563            state.serialize_entry("metadata", &self.metadata)?;
564        }
565
566        match &self.inner {
567            DelimitedBlockType::DelimitedStem(stem) => {
568                state.serialize_entry("content", &stem.content)?;
569                state.serialize_entry("notation", &stem.notation)?;
570            }
571            DelimitedBlockType::DelimitedListing(inner)
572            | DelimitedBlockType::DelimitedLiteral(inner)
573            | DelimitedBlockType::DelimitedPass(inner)
574            | DelimitedBlockType::DelimitedVerse(inner) => {
575                state.serialize_entry("inlines", &inner)?;
576            }
577            DelimitedBlockType::DelimitedTable(inner) => {
578                state.serialize_entry("content", &inner)?;
579            }
580            inner @ (DelimitedBlockType::DelimitedComment(_)
581            | DelimitedBlockType::DelimitedExample(_)
582            | DelimitedBlockType::DelimitedOpen(_)
583            | DelimitedBlockType::DelimitedQuote(_)
584            | DelimitedBlockType::DelimitedSidebar(_)) => {
585                state.serialize_entry("blocks", &inner)?;
586            }
587        }
588        if !self.title.is_empty() {
589            state.serialize_entry("title", &self.title)?;
590        }
591        state.serialize_entry("location", &self.location)?;
592        state.end()
593    }
594}
595
596impl Serialize for DiscreteHeader {
597    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
598    where
599        S: Serializer,
600    {
601        let mut state = serializer.serialize_map(None)?;
602        state.serialize_entry("name", "heading")?;
603        state.serialize_entry("type", "block")?;
604        if !self.title.is_empty() {
605            state.serialize_entry("title", &self.title)?;
606        }
607        state.serialize_entry("level", &self.level)?;
608        if !self.metadata.is_default() {
609            state.serialize_entry("metadata", &self.metadata)?;
610        }
611        state.serialize_entry("location", &self.location)?;
612        state.end()
613    }
614}
615
616impl Serialize for Paragraph {
617    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
618    where
619        S: Serializer,
620    {
621        let mut state = serializer.serialize_map(None)?;
622        state.serialize_entry("name", "paragraph")?;
623        state.serialize_entry("type", "block")?;
624        if !self.title.is_empty() {
625            state.serialize_entry("title", &self.title)?;
626        }
627        state.serialize_entry("inlines", &self.content)?;
628        if !self.metadata.is_default() {
629            state.serialize_entry("metadata", &self.metadata)?;
630        }
631        state.serialize_entry("location", &self.location)?;
632        state.end()
633    }
634}
635
636// =============================================================================
637// Block Deserialization Infrastructure
638// =============================================================================
639
640/// Raw field collector for Block deserialization.
641/// Uses derived Deserialize to handle JSON field parsing, then dispatches to constructors.
642#[derive(Default, Deserialize)]
643#[serde(default)]
644struct RawBlockFields {
645    name: Option<String>,
646    r#type: Option<String>,
647    value: Option<String>,
648    form: Option<String>,
649    target: Option<String>,
650    source: Option<Source>,
651    sources: Option<Vec<Source>>,
652    delimiter: Option<String>,
653    reftext: Option<String>,
654    id: Option<String>,
655    title: Option<Vec<InlineNode>>,
656    anchors: Option<Vec<Anchor>>,
657    level: Option<SectionLevel>,
658    metadata: Option<BlockMetadata>,
659    variant: Option<String>,
660    content: Option<serde_json::Value>,
661    notation: Option<serde_json::Value>,
662    blocks: Option<serde_json::Value>,
663    inlines: Option<Vec<InlineNode>>,
664    marker: Option<String>,
665    items: Option<serde_json::Value>,
666    location: Option<Location>,
667}
668
669/// Helper to parse `Vec<Block>` from `serde_json::Value`
670fn parse_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
671    match value {
672        Some(serde_json::Value::Array(arr)) => arr
673            .into_iter()
674            .map(|v| serde_json::from_value(v).map_err(E::custom))
675            .collect(),
676        Some(_) => Err(E::custom("blocks must be an array")),
677        None => Ok(Vec::new()),
678    }
679}
680
681/// Helper to require `Vec<Block>` from `serde_json::Value`
682fn require_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
683    match value {
684        Some(serde_json::Value::Array(arr)) => arr
685            .into_iter()
686            .map(|v| serde_json::from_value(v).map_err(E::custom))
687            .collect(),
688        Some(_) => Err(E::custom("blocks must be an array")),
689        None => Err(E::missing_field("blocks")),
690    }
691}
692
693/// Helper to parse `Vec<ListItem>` from `serde_json::Value`
694fn parse_list_items<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<ListItem>, E> {
695    match value {
696        Some(serde_json::Value::Array(arr)) => arr
697            .into_iter()
698            .map(|v| serde_json::from_value(v).map_err(E::custom))
699            .collect(),
700        Some(_) => Err(E::custom("items must be an array")),
701        None => Err(E::missing_field("items")),
702    }
703}
704
705/// Helper to parse `Vec<DescriptionListItem>` from `serde_json::Value`
706fn parse_dlist_items<E: de::Error>(
707    value: Option<serde_json::Value>,
708) -> Result<Vec<DescriptionListItem>, E> {
709    match value {
710        Some(serde_json::Value::Array(arr)) => arr
711            .into_iter()
712            .map(|v| serde_json::from_value(v).map_err(E::custom))
713            .collect(),
714        Some(_) => Err(E::custom("items must be an array")),
715        None => Err(E::missing_field("items")),
716    }
717}
718
719// -----------------------------------------------------------------------------
720// Per-variant Block constructors
721// -----------------------------------------------------------------------------
722
723fn construct_section<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
724    Ok(Block::Section(Section {
725        metadata: raw.metadata.unwrap_or_default(),
726        title: raw.title.unwrap_or_default().into(),
727        level: raw.level.ok_or_else(|| E::missing_field("level"))?,
728        content: parse_blocks(raw.blocks)?,
729        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
730    }))
731}
732
733fn construct_paragraph<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
734    Ok(Block::Paragraph(Paragraph {
735        metadata: raw.metadata.unwrap_or_default(),
736        title: raw.title.unwrap_or_default().into(),
737        content: raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
738        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
739    }))
740}
741
742fn construct_image<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
743    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
744    if form != "macro" {
745        return Err(E::custom(format!("unexpected form: {form}")));
746    }
747    Ok(Block::Image(Image {
748        title: raw.title.unwrap_or_default().into(),
749        source: raw.source.ok_or_else(|| E::missing_field("source"))?,
750        metadata: raw.metadata.unwrap_or_default(),
751        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
752    }))
753}
754
755fn construct_audio<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
756    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
757    if form != "macro" {
758        return Err(E::custom(format!("unexpected form: {form}")));
759    }
760    Ok(Block::Audio(Audio {
761        title: raw.title.unwrap_or_default().into(),
762        source: raw.source.ok_or_else(|| E::missing_field("source"))?,
763        metadata: raw.metadata.unwrap_or_default(),
764        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
765    }))
766}
767
768fn construct_video<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
769    let sources = if let Some(sources_value) = raw.sources {
770        sources_value
771    } else {
772        // Fallback to simplified format with target
773        let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
774        if form != "macro" {
775            return Err(E::custom(format!("unexpected form: {form}")));
776        }
777        let target = raw.target.ok_or_else(|| E::missing_field("target"))?;
778        let source = Source::from_str(&target).map_err(E::custom)?;
779        vec![source]
780    };
781    Ok(Block::Video(Video {
782        title: raw.title.unwrap_or_default().into(),
783        sources,
784        metadata: raw.metadata.unwrap_or_default(),
785        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
786    }))
787}
788
789fn construct_break<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
790    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
791    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
792    match variant.as_str() {
793        "page" => Ok(Block::PageBreak(PageBreak {
794            title: raw.title.unwrap_or_default().into(),
795            metadata: raw.metadata.unwrap_or_default(),
796            location,
797        })),
798        "thematic" => Ok(Block::ThematicBreak(ThematicBreak {
799            title: raw.title.unwrap_or_default().into(),
800            anchors: raw.anchors.unwrap_or_default(),
801            location,
802        })),
803        _ => Err(E::custom(format!("unexpected 'break' variant: {variant}"))),
804    }
805}
806
807fn construct_heading<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
808    Ok(Block::DiscreteHeader(DiscreteHeader {
809        title: raw.title.unwrap_or_default().into(),
810        level: raw.level.ok_or_else(|| E::missing_field("level"))?,
811        metadata: raw.metadata.unwrap_or_default(),
812        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
813    }))
814}
815
816fn construct_toc<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
817    Ok(Block::TableOfContents(TableOfContents {
818        metadata: raw.metadata.unwrap_or_default(),
819        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
820    }))
821}
822
823fn construct_comment<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
824    let content = match raw.content {
825        Some(serde_json::Value::String(s)) => s,
826        Some(_) => return Err(E::custom("comment content must be a string")),
827        None => String::new(),
828    };
829    Ok(Block::Comment(Comment {
830        content,
831        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
832    }))
833}
834
835fn construct_admonition<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
836    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
837    Ok(Block::Admonition(Admonition {
838        metadata: raw.metadata.unwrap_or_default(),
839        variant: AdmonitionVariant::from_str(&variant).map_err(E::custom)?,
840        blocks: require_blocks(raw.blocks)?,
841        title: raw.title.unwrap_or_default().into(),
842        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
843    }))
844}
845
846fn construct_dlist<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
847    Ok(Block::DescriptionList(DescriptionList {
848        title: raw.title.unwrap_or_default().into(),
849        metadata: raw.metadata.unwrap_or_default(),
850        items: parse_dlist_items(raw.items)?,
851        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
852    }))
853}
854
855fn construct_list<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
856    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
857    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
858    let title: Title = raw.title.unwrap_or_default().into();
859    let metadata = raw.metadata.unwrap_or_default();
860    let items = parse_list_items(raw.items)?;
861
862    match variant.as_str() {
863        "unordered" => Ok(Block::UnorderedList(UnorderedList {
864            title,
865            metadata,
866            marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
867            items,
868            location,
869        })),
870        "ordered" => Ok(Block::OrderedList(OrderedList {
871            title,
872            metadata,
873            marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
874            items,
875            location,
876        })),
877        "callout" => Ok(Block::CalloutList(CalloutList {
878            title,
879            metadata,
880            items,
881            location,
882        })),
883        _ => Err(E::custom(format!("unexpected 'list' variant: {variant}"))),
884    }
885}
886
887fn construct_delimited<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
888    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
889    if form != "delimited" {
890        return Err(E::custom(format!("unexpected form: {form}")));
891    }
892    let delimiter = raw.delimiter.ok_or_else(|| E::missing_field("delimiter"))?;
893    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
894    let metadata = raw.metadata.unwrap_or_default();
895    let title: Title = raw.title.unwrap_or_default().into();
896
897    let inner = match name {
898        "example" => DelimitedBlockType::DelimitedExample(require_blocks(raw.blocks)?),
899        "sidebar" => DelimitedBlockType::DelimitedSidebar(require_blocks(raw.blocks)?),
900        "open" => DelimitedBlockType::DelimitedOpen(require_blocks(raw.blocks)?),
901        "quote" => DelimitedBlockType::DelimitedQuote(require_blocks(raw.blocks)?),
902        "verse" => DelimitedBlockType::DelimitedVerse(
903            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
904        ),
905        "listing" => DelimitedBlockType::DelimitedListing(
906            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
907        ),
908        "literal" => DelimitedBlockType::DelimitedLiteral(
909            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
910        ),
911        "pass" => DelimitedBlockType::DelimitedPass(
912            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
913        ),
914        "stem" => {
915            let serde_json::Value::String(content) =
916                raw.content.ok_or_else(|| E::missing_field("content"))?
917            else {
918                return Err(E::custom("content must be a string"));
919            };
920            let notation = match raw.notation {
921                Some(serde_json::Value::String(n)) => {
922                    StemNotation::from_str(&n).map_err(E::custom)?
923                }
924                Some(
925                    serde_json::Value::Null
926                    | serde_json::Value::Bool(_)
927                    | serde_json::Value::Number(_)
928                    | serde_json::Value::Array(_)
929                    | serde_json::Value::Object(_),
930                )
931                | None => StemNotation::Latexmath,
932            };
933            DelimitedBlockType::DelimitedStem(StemContent { content, notation })
934        }
935        "table" => {
936            let table =
937                serde_json::from_value(raw.content.ok_or_else(|| E::missing_field("content"))?)
938                    .map_err(|e| {
939                        tracing::error!("content must be compatible with `Table` type: {e}");
940                        E::custom("content must be compatible with `Table` type")
941                    })?;
942            DelimitedBlockType::DelimitedTable(table)
943        }
944        _ => return Err(E::custom(format!("unexpected delimited block: {name}"))),
945    };
946
947    Ok(Block::DelimitedBlock(DelimitedBlock {
948        metadata,
949        inner,
950        delimiter,
951        title,
952        location,
953    }))
954}
955
956fn construct_document_attribute<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
957    let value = if let Some(value) = raw.value {
958        if value.is_empty() {
959            AttributeValue::None
960        } else if value.eq_ignore_ascii_case("true") {
961            AttributeValue::Bool(true)
962        } else if value.eq_ignore_ascii_case("false") {
963            AttributeValue::Bool(false)
964        } else {
965            AttributeValue::String(value)
966        }
967    } else {
968        AttributeValue::None
969    };
970    Ok(Block::DocumentAttribute(DocumentAttribute {
971        name: name.to_string(),
972        value,
973        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
974    }))
975}
976
977/// Dispatch to the appropriate Block constructor based on name/type
978fn dispatch_block<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
979    // Take ownership of name/type for dispatch, avoiding borrow issues
980    let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
981    let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
982
983    match (name.as_str(), ty.as_str()) {
984        ("section", "block") => construct_section(raw),
985        ("paragraph", "block") => construct_paragraph(raw),
986        ("image", "block") => construct_image(raw),
987        ("audio", "block") => construct_audio(raw),
988        ("video", "block") => construct_video(raw),
989        ("break", "block") => construct_break(raw),
990        ("heading", "block") => construct_heading(raw),
991        ("toc", "block") => construct_toc(raw),
992        ("comment", "block") => construct_comment(raw),
993        ("admonition", "block") => construct_admonition(raw),
994        ("dlist", "block") => construct_dlist(raw),
995        ("list", "block") => construct_list(raw),
996        // Delimited blocks
997        (
998            "example" | "sidebar" | "open" | "quote" | "verse" | "listing" | "literal" | "pass"
999            | "stem" | "table",
1000            "block",
1001        ) => construct_delimited(&name, raw),
1002        // Document attribute (type != "block")
1003        (_, "attribute") => construct_document_attribute(&name, raw),
1004        _ => Err(E::custom(format!(
1005            "unexpected name/type combination: {name}/{ty}"
1006        ))),
1007    }
1008}
1009
1010impl<'de> Deserialize<'de> for Block {
1011    fn deserialize<D>(deserializer: D) -> Result<Block, D::Error>
1012    where
1013        D: Deserializer<'de>,
1014    {
1015        // Deserialize into RawBlockFields using derived Deserialize, then dispatch
1016        let raw: RawBlockFields = RawBlockFields::deserialize(deserializer)?;
1017        dispatch_block(raw)
1018    }
1019}