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, CalloutListItem, DescriptionList, DescriptionListItem, ListItem,
29    ListItemCheckedStatus, ListLevel, 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<CalloutListItem>` from `serde_json::Value`
752fn parse_callout_list_items<E: de::Error>(
753    value: Option<serde_json::Value>,
754) -> Result<Vec<CalloutListItem>, 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/// Helper to parse `Vec<DescriptionListItem>` from `serde_json::Value`
766fn parse_dlist_items<E: de::Error>(
767    value: Option<serde_json::Value>,
768) -> Result<Vec<DescriptionListItem>, E> {
769    match value {
770        Some(serde_json::Value::Array(arr)) => arr
771            .into_iter()
772            .map(|v| serde_json::from_value(v).map_err(E::custom))
773            .collect(),
774        Some(_) => Err(E::custom("items must be an array")),
775        None => Err(E::missing_field("items")),
776    }
777}
778
779// -----------------------------------------------------------------------------
780// Per-variant Block constructors
781// -----------------------------------------------------------------------------
782
783fn construct_section<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
784    Ok(Block::Section(Section {
785        metadata: raw.metadata.unwrap_or_default(),
786        title: raw.title.unwrap_or_default().into(),
787        level: raw.level.ok_or_else(|| E::missing_field("level"))?,
788        content: parse_blocks(raw.blocks)?,
789        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
790    }))
791}
792
793fn construct_paragraph<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
794    Ok(Block::Paragraph(Paragraph {
795        metadata: raw.metadata.unwrap_or_default(),
796        title: raw.title.unwrap_or_default().into(),
797        content: raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
798        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
799    }))
800}
801
802fn construct_image<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
803    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
804    if form != "macro" {
805        return Err(E::custom(format!("unexpected form: {form}")));
806    }
807    Ok(Block::Image(Image {
808        title: raw.title.unwrap_or_default().into(),
809        source: raw.source.ok_or_else(|| E::missing_field("source"))?,
810        metadata: raw.metadata.unwrap_or_default(),
811        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
812    }))
813}
814
815fn construct_audio<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
816    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
817    if form != "macro" {
818        return Err(E::custom(format!("unexpected form: {form}")));
819    }
820    Ok(Block::Audio(Audio {
821        title: raw.title.unwrap_or_default().into(),
822        source: raw.source.ok_or_else(|| E::missing_field("source"))?,
823        metadata: raw.metadata.unwrap_or_default(),
824        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
825    }))
826}
827
828fn construct_video<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
829    let sources = if let Some(sources_value) = raw.sources {
830        sources_value
831    } else {
832        // Fallback to simplified format with target
833        let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
834        if form != "macro" {
835            return Err(E::custom(format!("unexpected form: {form}")));
836        }
837        let target = raw.target.ok_or_else(|| E::missing_field("target"))?;
838        let source = Source::from_str(&target).map_err(E::custom)?;
839        vec![source]
840    };
841    Ok(Block::Video(Video {
842        title: raw.title.unwrap_or_default().into(),
843        sources,
844        metadata: raw.metadata.unwrap_or_default(),
845        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
846    }))
847}
848
849fn construct_break<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
850    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
851    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
852    match variant.as_str() {
853        "page" => Ok(Block::PageBreak(PageBreak {
854            title: raw.title.unwrap_or_default().into(),
855            metadata: raw.metadata.unwrap_or_default(),
856            location,
857        })),
858        "thematic" => Ok(Block::ThematicBreak(ThematicBreak {
859            title: raw.title.unwrap_or_default().into(),
860            anchors: raw.anchors.unwrap_or_default(),
861            location,
862        })),
863        _ => Err(E::custom(format!("unexpected 'break' variant: {variant}"))),
864    }
865}
866
867fn construct_heading<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
868    Ok(Block::DiscreteHeader(DiscreteHeader {
869        title: raw.title.unwrap_or_default().into(),
870        level: raw.level.ok_or_else(|| E::missing_field("level"))?,
871        metadata: raw.metadata.unwrap_or_default(),
872        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
873    }))
874}
875
876fn construct_toc<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
877    Ok(Block::TableOfContents(TableOfContents {
878        metadata: raw.metadata.unwrap_or_default(),
879        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
880    }))
881}
882
883fn construct_comment<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
884    let content = match raw.content {
885        Some(serde_json::Value::String(s)) => s,
886        Some(_) => return Err(E::custom("comment content must be a string")),
887        None => String::new(),
888    };
889    Ok(Block::Comment(Comment {
890        content,
891        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
892    }))
893}
894
895fn construct_admonition<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
896    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
897    Ok(Block::Admonition(Admonition {
898        metadata: raw.metadata.unwrap_or_default(),
899        variant: AdmonitionVariant::from_str(&variant).map_err(E::custom)?,
900        blocks: require_blocks(raw.blocks)?,
901        title: raw.title.unwrap_or_default().into(),
902        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
903    }))
904}
905
906fn construct_dlist<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
907    Ok(Block::DescriptionList(DescriptionList {
908        title: raw.title.unwrap_or_default().into(),
909        metadata: raw.metadata.unwrap_or_default(),
910        items: parse_dlist_items(raw.items)?,
911        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
912    }))
913}
914
915fn construct_list<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
916    let variant = raw.variant.ok_or_else(|| E::missing_field("variant"))?;
917    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
918    let title: Title = raw.title.unwrap_or_default().into();
919    let metadata = raw.metadata.unwrap_or_default();
920
921    match variant.as_str() {
922        "unordered" => {
923            let items = parse_list_items(raw.items)?;
924            Ok(Block::UnorderedList(UnorderedList {
925                title,
926                metadata,
927                marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
928                items,
929                location,
930            }))
931        }
932        "ordered" => {
933            let items = parse_list_items(raw.items)?;
934            Ok(Block::OrderedList(OrderedList {
935                title,
936                metadata,
937                marker: raw.marker.ok_or_else(|| E::missing_field("marker"))?,
938                items,
939                location,
940            }))
941        }
942        "callout" => {
943            let items = parse_callout_list_items(raw.items)?;
944            Ok(Block::CalloutList(CalloutList {
945                title,
946                metadata,
947                items,
948                location,
949            }))
950        }
951        _ => Err(E::custom(format!("unexpected 'list' variant: {variant}"))),
952    }
953}
954
955fn construct_delimited<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
956    let form = raw.form.ok_or_else(|| E::missing_field("form"))?;
957    if form != "delimited" {
958        return Err(E::custom(format!("unexpected form: {form}")));
959    }
960    let delimiter = raw.delimiter.ok_or_else(|| E::missing_field("delimiter"))?;
961    let location = raw.location.ok_or_else(|| E::missing_field("location"))?;
962    let metadata = raw.metadata.unwrap_or_default();
963    let title: Title = raw.title.unwrap_or_default().into();
964
965    let inner = match name {
966        "example" => DelimitedBlockType::DelimitedExample(require_blocks(raw.blocks)?),
967        "sidebar" => DelimitedBlockType::DelimitedSidebar(require_blocks(raw.blocks)?),
968        "open" => DelimitedBlockType::DelimitedOpen(require_blocks(raw.blocks)?),
969        "quote" => DelimitedBlockType::DelimitedQuote(require_blocks(raw.blocks)?),
970        "verse" => DelimitedBlockType::DelimitedVerse(
971            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
972        ),
973        "listing" => DelimitedBlockType::DelimitedListing(
974            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
975        ),
976        "literal" => DelimitedBlockType::DelimitedLiteral(
977            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
978        ),
979        "pass" => DelimitedBlockType::DelimitedPass(
980            raw.inlines.ok_or_else(|| E::missing_field("inlines"))?,
981        ),
982        "stem" => {
983            let serde_json::Value::String(content) =
984                raw.content.ok_or_else(|| E::missing_field("content"))?
985            else {
986                return Err(E::custom("content must be a string"));
987            };
988            let notation = match raw.notation {
989                Some(serde_json::Value::String(n)) => {
990                    StemNotation::from_str(&n).map_err(E::custom)?
991                }
992                Some(
993                    serde_json::Value::Null
994                    | serde_json::Value::Bool(_)
995                    | serde_json::Value::Number(_)
996                    | serde_json::Value::Array(_)
997                    | serde_json::Value::Object(_),
998                )
999                | None => StemNotation::Latexmath,
1000            };
1001            DelimitedBlockType::DelimitedStem(StemContent { content, notation })
1002        }
1003        "table" => {
1004            let table =
1005                serde_json::from_value(raw.content.ok_or_else(|| E::missing_field("content"))?)
1006                    .map_err(|e| {
1007                        tracing::error!("content must be compatible with `Table` type: {e}");
1008                        E::custom("content must be compatible with `Table` type")
1009                    })?;
1010            DelimitedBlockType::DelimitedTable(table)
1011        }
1012        _ => return Err(E::custom(format!("unexpected delimited block: {name}"))),
1013    };
1014
1015    Ok(Block::DelimitedBlock(DelimitedBlock {
1016        metadata,
1017        inner,
1018        delimiter,
1019        title,
1020        location,
1021    }))
1022}
1023
1024fn construct_document_attribute<E: de::Error>(name: &str, raw: RawBlockFields) -> Result<Block, E> {
1025    let value = if let Some(value) = raw.value {
1026        if value.is_empty() {
1027            AttributeValue::None
1028        } else if value.eq_ignore_ascii_case("true") {
1029            AttributeValue::Bool(true)
1030        } else if value.eq_ignore_ascii_case("false") {
1031            AttributeValue::Bool(false)
1032        } else {
1033            AttributeValue::String(value)
1034        }
1035    } else {
1036        AttributeValue::None
1037    };
1038    Ok(Block::DocumentAttribute(DocumentAttribute {
1039        name: name.to_string(),
1040        value,
1041        location: raw.location.ok_or_else(|| E::missing_field("location"))?,
1042    }))
1043}
1044
1045/// Dispatch to the appropriate Block constructor based on name/type
1046fn dispatch_block<E: de::Error>(raw: RawBlockFields) -> Result<Block, E> {
1047    // Take ownership of name/type for dispatch, avoiding borrow issues
1048    let name = raw.name.clone().ok_or_else(|| E::missing_field("name"))?;
1049    let ty = raw.r#type.clone().ok_or_else(|| E::missing_field("type"))?;
1050
1051    match (name.as_str(), ty.as_str()) {
1052        ("section", "block") => construct_section(raw),
1053        ("paragraph", "block") => construct_paragraph(raw),
1054        ("image", "block") => construct_image(raw),
1055        ("audio", "block") => construct_audio(raw),
1056        ("video", "block") => construct_video(raw),
1057        ("break", "block") => construct_break(raw),
1058        ("heading", "block") => construct_heading(raw),
1059        ("toc", "block") => construct_toc(raw),
1060        ("comment", "block") => construct_comment(raw),
1061        ("admonition", "block") => construct_admonition(raw),
1062        ("dlist", "block") => construct_dlist(raw),
1063        ("list", "block") => construct_list(raw),
1064        // Delimited blocks
1065        (
1066            "example" | "sidebar" | "open" | "quote" | "verse" | "listing" | "literal" | "pass"
1067            | "stem" | "table",
1068            "block",
1069        ) => construct_delimited(&name, raw),
1070        // Document attribute (type != "block")
1071        (_, "attribute") => construct_document_attribute(&name, raw),
1072        _ => Err(E::custom(format!(
1073            "unexpected name/type combination: {name}/{ty}"
1074        ))),
1075    }
1076}
1077
1078impl<'de> Deserialize<'de> for Block {
1079    fn deserialize<D>(deserializer: D) -> Result<Block, D::Error>
1080    where
1081        D: Deserializer<'de>,
1082    {
1083        // Deserialize into RawBlockFields using derived Deserialize, then dispatch
1084        let raw: RawBlockFields = RawBlockFields::deserialize(deserializer)?;
1085        dispatch_block(raw)
1086    }
1087}