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/// The inner content type of a delimited block.
493///
494/// Each variant wraps the content appropriate for that block type:
495/// - **Verbatim content** (`Vec<InlineNode>`): `DelimitedListing`, `DelimitedLiteral`,
496///   `DelimitedPass`, `DelimitedVerse`, `DelimitedComment` - preserves whitespace/formatting
497/// - **Compound content** (`Vec<Block>`): `DelimitedExample`, `DelimitedOpen`,
498///   `DelimitedSidebar`, `DelimitedQuote` - can contain nested blocks
499/// - **Structured content**: `DelimitedTable(Table)`, `DelimitedStem(StemContent)`
500///
501/// # Accessing Content
502///
503/// Use pattern matching to extract the inner content:
504///
505/// ```
506/// # use acdc_parser::{DelimitedBlockType, Block, InlineNode};
507/// fn process_block(block_type: &DelimitedBlockType) {
508///     match block_type {
509///         DelimitedBlockType::DelimitedListing(inlines) => {
510///             // Handle listing content (source code, etc.)
511///         }
512///         DelimitedBlockType::DelimitedExample(blocks) => {
513///             // Handle example with nested blocks
514///         }
515///         DelimitedBlockType::DelimitedTable(table) => {
516///             // Access table.rows, table.header, etc.
517///         }
518///         // ... other variants
519///         _ => {}
520///     }
521/// }
522/// ```
523///
524/// # Note on Variant Names
525///
526/// Variants are prefixed with `Delimited` to disambiguate from potential future
527/// non-delimited block types and to make pattern matching more explicit.
528#[non_exhaustive]
529#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
530#[serde(untagged)]
531pub enum DelimitedBlockType {
532    /// Comment block content (not rendered in output).
533    DelimitedComment(Vec<InlineNode>),
534    /// Example block - can contain nested blocks, admonitions, etc.
535    DelimitedExample(Vec<Block>),
536    /// Listing block - typically source code with syntax highlighting.
537    DelimitedListing(Vec<InlineNode>),
538    /// Literal block - preformatted text rendered verbatim.
539    DelimitedLiteral(Vec<InlineNode>),
540    /// Open block - generic container for nested blocks.
541    DelimitedOpen(Vec<Block>),
542    /// Sidebar block - supplementary content in a styled container.
543    DelimitedSidebar(Vec<Block>),
544    /// Table block - structured tabular data.
545    DelimitedTable(Table),
546    /// Passthrough block - content passed directly to output without processing.
547    DelimitedPass(Vec<InlineNode>),
548    /// Quote block - blockquote with optional attribution.
549    DelimitedQuote(Vec<Block>),
550    /// Verse block - poetry/lyrics preserving line breaks.
551    DelimitedVerse(Vec<InlineNode>),
552    /// STEM (math) block - LaTeX or `AsciiMath` notation.
553    DelimitedStem(StemContent),
554}
555
556impl DelimitedBlockType {
557    fn name(&self) -> &'static str {
558        match self {
559            DelimitedBlockType::DelimitedComment(_) => "comment",
560            DelimitedBlockType::DelimitedExample(_) => "example",
561            DelimitedBlockType::DelimitedListing(_) => "listing",
562            DelimitedBlockType::DelimitedLiteral(_) => "literal",
563            DelimitedBlockType::DelimitedOpen(_) => "open",
564            DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
565            DelimitedBlockType::DelimitedTable(_) => "table",
566            DelimitedBlockType::DelimitedPass(_) => "pass",
567            DelimitedBlockType::DelimitedQuote(_) => "quote",
568            DelimitedBlockType::DelimitedVerse(_) => "verse",
569            DelimitedBlockType::DelimitedStem(_) => "stem",
570        }
571    }
572}
573
574impl Serialize for Document {
575    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
576    where
577        S: Serializer,
578    {
579        let mut state = serializer.serialize_map(None)?;
580        state.serialize_entry("name", "document")?;
581        state.serialize_entry("type", "block")?;
582        if let Some(header) = &self.header {
583            state.serialize_entry("header", header)?;
584            // We serialize the attributes even if they're empty because that's what the
585            // TCK expects (odd but true)
586            state.serialize_entry("attributes", &self.attributes)?;
587        } else if !self.attributes.is_empty() {
588            state.serialize_entry("attributes", &self.attributes)?;
589        }
590        if !self.blocks.is_empty() {
591            state.serialize_entry("blocks", &self.blocks)?;
592        }
593        state.serialize_entry("location", &self.location)?;
594        state.end()
595    }
596}
597
598impl Serialize for DelimitedBlock {
599    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
600    where
601        S: Serializer,
602    {
603        let mut state = serializer.serialize_map(None)?;
604        state.serialize_entry("name", self.inner.name())?;
605        state.serialize_entry("type", "block")?;
606        state.serialize_entry("form", "delimited")?;
607        state.serialize_entry("delimiter", &self.delimiter)?;
608        if !self.metadata.is_default() {
609            state.serialize_entry("metadata", &self.metadata)?;
610        }
611
612        match &self.inner {
613            DelimitedBlockType::DelimitedStem(stem) => {
614                state.serialize_entry("content", &stem.content)?;
615                state.serialize_entry("notation", &stem.notation)?;
616            }
617            DelimitedBlockType::DelimitedListing(inner)
618            | DelimitedBlockType::DelimitedLiteral(inner)
619            | DelimitedBlockType::DelimitedPass(inner)
620            | DelimitedBlockType::DelimitedVerse(inner) => {
621                state.serialize_entry("inlines", &inner)?;
622            }
623            DelimitedBlockType::DelimitedTable(inner) => {
624                state.serialize_entry("content", &inner)?;
625            }
626            inner @ (DelimitedBlockType::DelimitedComment(_)
627            | DelimitedBlockType::DelimitedExample(_)
628            | DelimitedBlockType::DelimitedOpen(_)
629            | DelimitedBlockType::DelimitedQuote(_)
630            | DelimitedBlockType::DelimitedSidebar(_)) => {
631                state.serialize_entry("blocks", &inner)?;
632            }
633        }
634        if !self.title.is_empty() {
635            state.serialize_entry("title", &self.title)?;
636        }
637        state.serialize_entry("location", &self.location)?;
638        state.end()
639    }
640}
641
642impl Serialize for DiscreteHeader {
643    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
644    where
645        S: Serializer,
646    {
647        let mut state = serializer.serialize_map(None)?;
648        state.serialize_entry("name", "heading")?;
649        state.serialize_entry("type", "block")?;
650        if !self.title.is_empty() {
651            state.serialize_entry("title", &self.title)?;
652        }
653        state.serialize_entry("level", &self.level)?;
654        if !self.metadata.is_default() {
655            state.serialize_entry("metadata", &self.metadata)?;
656        }
657        state.serialize_entry("location", &self.location)?;
658        state.end()
659    }
660}
661
662impl Serialize for Paragraph {
663    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
664    where
665        S: Serializer,
666    {
667        let mut state = serializer.serialize_map(None)?;
668        state.serialize_entry("name", "paragraph")?;
669        state.serialize_entry("type", "block")?;
670        if !self.title.is_empty() {
671            state.serialize_entry("title", &self.title)?;
672        }
673        state.serialize_entry("inlines", &self.content)?;
674        if !self.metadata.is_default() {
675            state.serialize_entry("metadata", &self.metadata)?;
676        }
677        state.serialize_entry("location", &self.location)?;
678        state.end()
679    }
680}
681
682// =============================================================================
683// Block Deserialization Infrastructure
684// =============================================================================
685
686/// Raw field collector for Block deserialization.
687/// Uses derived Deserialize to handle JSON field parsing, then dispatches to constructors.
688#[derive(Default, Deserialize)]
689#[serde(default)]
690struct RawBlockFields {
691    name: Option<String>,
692    r#type: Option<String>,
693    value: Option<String>,
694    form: Option<String>,
695    target: Option<String>,
696    source: Option<Source>,
697    sources: Option<Vec<Source>>,
698    delimiter: Option<String>,
699    reftext: Option<String>,
700    id: Option<String>,
701    title: Option<Vec<InlineNode>>,
702    anchors: Option<Vec<Anchor>>,
703    level: Option<SectionLevel>,
704    metadata: Option<BlockMetadata>,
705    variant: Option<String>,
706    content: Option<serde_json::Value>,
707    notation: Option<serde_json::Value>,
708    blocks: Option<serde_json::Value>,
709    inlines: Option<Vec<InlineNode>>,
710    marker: Option<String>,
711    items: Option<serde_json::Value>,
712    location: Option<Location>,
713}
714
715/// Helper to parse `Vec<Block>` from `serde_json::Value`
716fn parse_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
717    match value {
718        Some(serde_json::Value::Array(arr)) => arr
719            .into_iter()
720            .map(|v| serde_json::from_value(v).map_err(E::custom))
721            .collect(),
722        Some(_) => Err(E::custom("blocks must be an array")),
723        None => Ok(Vec::new()),
724    }
725}
726
727/// Helper to require `Vec<Block>` from `serde_json::Value`
728fn require_blocks<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<Block>, E> {
729    match value {
730        Some(serde_json::Value::Array(arr)) => arr
731            .into_iter()
732            .map(|v| serde_json::from_value(v).map_err(E::custom))
733            .collect(),
734        Some(_) => Err(E::custom("blocks must be an array")),
735        None => Err(E::missing_field("blocks")),
736    }
737}
738
739/// Helper to parse `Vec<ListItem>` from `serde_json::Value`
740fn parse_list_items<E: de::Error>(value: Option<serde_json::Value>) -> Result<Vec<ListItem>, E> {
741    match value {
742        Some(serde_json::Value::Array(arr)) => arr
743            .into_iter()
744            .map(|v| serde_json::from_value(v).map_err(E::custom))
745            .collect(),
746        Some(_) => Err(E::custom("items must be an array")),
747        None => Err(E::missing_field("items")),
748    }
749}
750
751/// Helper to parse `Vec<DescriptionListItem>` from `serde_json::Value`
752fn parse_dlist_items<E: de::Error>(
753    value: Option<serde_json::Value>,
754) -> Result<Vec<DescriptionListItem>, E> {
755    match value {
756        Some(serde_json::Value::Array(arr)) => arr
757            .into_iter()
758            .map(|v| serde_json::from_value(v).map_err(E::custom))
759            .collect(),
760        Some(_) => Err(E::custom("items must be an array")),
761        None => Err(E::missing_field("items")),
762    }
763}
764
765// -----------------------------------------------------------------------------
766// Per-variant Block constructors
767// -----------------------------------------------------------------------------
768
769fn construct_section<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
770    Ok(Block::Section(Section {
771        metadata: raw.metadata.unwrap_or_default(),
772        title: raw.title.unwrap_or_default().into(),
773        level: raw.level.ok_or_else(|| E::missing_field("level"))?,
774        content: parse_blocks(raw.blocks)?,
775        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
776    }))
777}
778
779fn construct_paragraph<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
780    Ok(Block::Paragraph(Paragraph {
781        metadata: raw.metadata.unwrap_or_default(),
782        title: raw.title.unwrap_or_default().into(),
783        content: raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
784        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
785    }))
786}
787
788fn construct_image<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
789    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
790    if form != "macro" {
791        return Err(E::custom(format!("unexpected form: {form}")));
792    }
793    Ok(Block::Image(Image {
794        title: raw.title.unwrap_or_default().into(),
795        source: raw.source.ok_or_else(|| E::missing_field("source"))?,
796        metadata: raw.metadata.unwrap_or_default(),
797        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
798    }))
799}
800
801fn construct_audio<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
802    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
803    if form != "macro" {
804        return Err(E::custom(format!("unexpected form: {form}")));
805    }
806    Ok(Block::Audio(Audio {
807        title: raw.title.unwrap_or_default().into(),
808        source: raw.source.ok_or_else(|| E::missing_field("source"))?,
809        metadata: raw.metadata.unwrap_or_default(),
810        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
811    }))
812}
813
814fn construct_video<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
815    let sources = if let Some(sources_value) = raw.sources {
816        sources_value
817    } else {
818        // Fallback to simplified format with target
819        let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
820        if form != "macro" {
821            return Err(E::custom(format!("unexpected form: {form}")));
822        }
823        let target = raw.target.ok_or_else(|| E::missing_field("target"))?;
824        let source = Source::from_str(&target).map_err(E::custom)?;
825        vec![source]
826    };
827    Ok(Block::Video(Video {
828        title: raw.title.unwrap_or_default().into(),
829        sources,
830        metadata: raw.metadata.unwrap_or_default(),
831        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
832    }))
833}
834
835fn construct_break<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
836    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
837    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
838    match variant.as_str() {
839        "page" => Ok(Block::PageBreak(PageBreak {
840            title: raw.title.unwrap_or_default().into(),
841            metadata: raw.metadata.unwrap_or_default(),
842            location,
843        })),
844        "thematic" => Ok(Block::ThematicBreak(ThematicBreak {
845            title: raw.title.unwrap_or_default().into(),
846            anchors: raw.anchors.unwrap_or_default(),
847            location,
848        })),
849        _ => Err(E::custom(format!("unexpected 'break' variant: {variant}"))),
850    }
851}
852
853fn construct_heading<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
854    Ok(Block::DiscreteHeader(DiscreteHeader {
855        title: raw.title.unwrap_or_default().into(),
856        level: raw.level.ok_or_else(|| E::missing_field("level"))?,
857        metadata: raw.metadata.unwrap_or_default(),
858        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
859    }))
860}
861
862fn construct_toc<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
863    Ok(Block::TableOfContents(TableOfContents {
864        metadata: raw.metadata.unwrap_or_default(),
865        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
866    }))
867}
868
869fn construct_comment<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
870    let content = match raw.content {
871        Some(serde_json::Value::String(s)) => s,
872        Some(_) => return Err(E::custom("comment content must be a string")),
873        None => String::new(),
874    };
875    Ok(Block::Comment(Comment {
876        content,
877        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
878    }))
879}
880
881fn construct_admonition<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
882    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
883    Ok(Block::Admonition(Admonition {
884        metadata: raw.metadata.unwrap_or_default(),
885        variant: AdmonitionVariant::from_str(&variant).map_err(E::custom)?,
886        blocks: require_blocks(raw.blocks)?,
887        title: raw.title.unwrap_or_default().into(),
888        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
889    }))
890}
891
892fn construct_dlist<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
893    Ok(Block::DescriptionList(DescriptionList {
894        title: raw.title.unwrap_or_default().into(),
895        metadata: raw.metadata.unwrap_or_default(),
896        items: parse_dlist_items(raw.items)?,
897        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
898    }))
899}
900
901fn construct_list<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
902    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
903    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
904    let title: Title = raw.title.unwrap_or_default().into();
905    let metadata = raw.metadata.unwrap_or_default();
906    let items = parse_list_items(raw.items)?;
907
908    match variant.as_str() {
909        "unordered" => Ok(Block::UnorderedList(UnorderedList {
910            title,
911            metadata,
912            marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
913            items,
914            location,
915        })),
916        "ordered" => Ok(Block::OrderedList(OrderedList {
917            title,
918            metadata,
919            marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
920            items,
921            location,
922        })),
923        "callout" => Ok(Block::CalloutList(CalloutList {
924            title,
925            metadata,
926            items,
927            location,
928        })),
929        _ => Err(E::custom(format!("unexpected 'list' variant: {variant}"))),
930    }
931}
932
933fn construct_delimited<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
934    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
935    if form != "delimited" {
936        return Err(E::custom(format!("unexpected form: {form}")));
937    }
938    let delimiter = raw.delimiter.ok_or_else(|| E::missing_field("delimiter"))?;
939    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
940    let metadata = raw.metadata.unwrap_or_default();
941    let title: Title = raw.title.unwrap_or_default().into();
942
943    let inner = match name {
944        "example" => DelimitedBlockType::DelimitedExample(require_blocks(raw.blocks)?),
945        "sidebar" => DelimitedBlockType::DelimitedSidebar(require_blocks(raw.blocks)?),
946        "open" => DelimitedBlockType::DelimitedOpen(require_blocks(raw.blocks)?),
947        "quote" => DelimitedBlockType::DelimitedQuote(require_blocks(raw.blocks)?),
948        "verse" => DelimitedBlockType::DelimitedVerse(
949            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
950        ),
951        "listing" => DelimitedBlockType::DelimitedListing(
952            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
953        ),
954        "literal" => DelimitedBlockType::DelimitedLiteral(
955            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
956        ),
957        "pass" => DelimitedBlockType::DelimitedPass(
958            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
959        ),
960        "stem" => {
961            let serde_json::Value::String(content) =
962                raw.content.ok_or_else(|| E::missing_field("content"))?
963            else {
964                return Err(E::custom("content must be a string"));
965            };
966            let notation = match raw.notation {
967                Some(serde_json::Value::String(n)) => {
968                    StemNotation::from_str(&n).map_err(E::custom)?
969                }
970                Some(
971                    serde_json::Value::Null
972                    | serde_json::Value::Bool(_)
973                    | serde_json::Value::Number(_)
974                    | serde_json::Value::Array(_)
975                    | serde_json::Value::Object(_),
976                )
977                | None => StemNotation::Latexmath,
978            };
979            DelimitedBlockType::DelimitedStem(StemContent { content, notation })
980        }
981        "table" => {
982            let table =
983                serde_json::from_value(raw.content.ok_or_else(|| E::missing_field("content"))?)
984                    .map_err(|e| {
985                        tracing::error!("content must be compatible with `Table` type: {e}");
986                        E::custom("content must be compatible with `Table` type")
987                    })?;
988            DelimitedBlockType::DelimitedTable(table)
989        }
990        _ => return Err(E::custom(format!("unexpected delimited block: {name}"))),
991    };
992
993    Ok(Block::DelimitedBlock(DelimitedBlock {
994        metadata,
995        inner,
996        delimiter,
997        title,
998        location,
999    }))
1000}
1001
1002fn construct_document_attribute<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
1003    let value = if let Some(value) = raw.value {
1004        if value.is_empty() {
1005            AttributeValue::None
1006        } else if value.eq_ignore_ascii_case("true") {
1007            AttributeValue::Bool(true)
1008        } else if value.eq_ignore_ascii_case("false") {
1009            AttributeValue::Bool(false)
1010        } else {
1011            AttributeValue::String(value)
1012        }
1013    } else {
1014        AttributeValue::None
1015    };
1016    Ok(Block::DocumentAttribute(DocumentAttribute {
1017        name: name.to_string(),
1018        value,
1019        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
1020    }))
1021}
1022
1023/// Dispatch to the appropriate Block constructor based on name/type
1024fn dispatch_block<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
1025    // Take ownership of name/type for dispatch, avoiding borrow issues
1026    let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
1027    let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
1028
1029    match (name.as_str(), ty.as_str()) {
1030        ("section", "block") => construct_section(raw),
1031        ("paragraph", "block") => construct_paragraph(raw),
1032        ("image", "block") => construct_image(raw),
1033        ("audio", "block") => construct_audio(raw),
1034        ("video", "block") => construct_video(raw),
1035        ("break", "block") => construct_break(raw),
1036        ("heading", "block") => construct_heading(raw),
1037        ("toc", "block") => construct_toc(raw),
1038        ("comment", "block") => construct_comment(raw),
1039        ("admonition", "block") => construct_admonition(raw),
1040        ("dlist", "block") => construct_dlist(raw),
1041        ("list", "block") => construct_list(raw),
1042        // Delimited blocks
1043        (
1044            "example" | "sidebar" | "open" | "quote" | "verse" | "listing" | "literal" | "pass"
1045            | "stem" | "table",
1046            "block",
1047        ) => construct_delimited(&name, raw),
1048        // Document attribute (type != "block")
1049        (_, "attribute") => construct_document_attribute(&name, raw),
1050        _ => Err(E::custom(format!(
1051            "unexpected name/type combination: {name}/{ty}"
1052        ))),
1053    }
1054}
1055
1056impl<'de> Deserialize<'de> for Block {
1057    fn deserialize<D>(deserializer: D) -> Result<Block, D::Error>
1058    where
1059        D: Deserializer<'de>,
1060    {
1061        // Deserialize into RawBlockFields using derived Deserialize, then dispatch
1062        let raw: RawBlockFields = RawBlockFields::deserialize(deserializer)?;
1063        dispatch_block(raw)
1064    }
1065}