Skip to main content

acdc_parser/model/
mod.rs

1//! The data models for the `AsciiDoc` document.
2use std::{fmt::Display, str::FromStr, string::ToString};
3
4use bumpalo::Bump;
5use serde::{
6    Serialize,
7    ser::{SerializeMap, Serializer},
8};
9
10mod admonition;
11mod anchor;
12mod attributes;
13mod attribution;
14mod inlines;
15mod lists;
16mod location;
17mod media;
18mod metadata;
19mod section;
20pub(crate) mod substitution;
21mod tables;
22mod title;
23
24pub use admonition::{Admonition, AdmonitionVariant};
25pub use anchor::{Anchor, TocEntry, UNNUMBERED_SECTION_STYLES};
26pub use attributes::{
27    AttributeName, AttributeValue, DocumentAttributes, ElementAttributes, MAX_SECTION_LEVELS,
28    MAX_TOC_LEVELS, strip_quotes,
29};
30pub use attribution::{Attribution, CiteTitle};
31pub use inlines::*;
32pub use lists::{
33    CalloutList, CalloutListItem, DescriptionList, DescriptionListItem, ListItem,
34    ListItemCheckedStatus, ListLevel, OrderedList, UnorderedList,
35};
36pub use location::*;
37pub use media::{Audio, Image, Source, SourceUrl, Video};
38pub use metadata::{BlockMetadata, Role};
39pub use section::*;
40pub use substitution::*;
41pub use tables::{
42    ColumnFormat, ColumnStyle, ColumnWidth, HorizontalAlignment, Table, TableColumn, TableRow,
43    VerticalAlignment,
44};
45pub use title::{Subtitle, Title};
46
47/// A `Document` represents the root of an `AsciiDoc` document.
48#[derive(Default, Debug, PartialEq)]
49#[non_exhaustive]
50pub struct Document<'a> {
51    pub header: Option<Header<'a>>,
52    pub attributes: DocumentAttributes<'a>,
53    pub blocks: Vec<Block<'a>>,
54    pub footnotes: Vec<Footnote<'a>>,
55    pub toc_entries: Vec<TocEntry<'a>>,
56    pub location: Location,
57}
58
59/// A `Header` represents the header of a document.
60///
61/// The header contains the title, subtitle, authors, and optional metadata
62/// (such as ID and roles) that can be applied to the document title.
63#[derive(Debug, PartialEq, Serialize)]
64#[non_exhaustive]
65pub struct Header<'a> {
66    #[serde(skip_serializing_if = "BlockMetadata::is_default")]
67    pub metadata: BlockMetadata<'a>,
68    #[serde(skip_serializing_if = "Title::is_empty")]
69    pub title: Title<'a>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub subtitle: Option<Subtitle<'a>>,
72    #[serde(skip_serializing_if = "Vec::is_empty")]
73    pub authors: Vec<Author<'a>>,
74    pub location: Location,
75}
76
77/// An `Author` represents the author of a document.
78#[derive(Debug, PartialEq, Serialize)]
79#[non_exhaustive]
80pub struct Author<'a> {
81    #[serde(rename = "firstname")]
82    pub first_name: &'a str,
83    #[serde(skip_serializing_if = "Option::is_none", rename = "middlename")]
84    pub middle_name: Option<&'a str>,
85    #[serde(rename = "lastname")]
86    pub last_name: &'a str,
87    pub initials: &'a str,
88    #[serde(skip_serializing_if = "Option::is_none", rename = "address")]
89    pub email: Option<&'a str>,
90}
91
92impl<'a> Header<'a> {
93    /// Create a new header with the given title and location.
94    #[must_use]
95    pub fn new(title: Title<'a>, location: Location) -> Self {
96        Self {
97            metadata: BlockMetadata::default(),
98            title,
99            subtitle: None,
100            authors: Vec::new(),
101            location,
102        }
103    }
104
105    /// Set the metadata.
106    #[must_use]
107    pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
108        self.metadata = metadata;
109        self
110    }
111
112    /// Set the subtitle.
113    #[must_use]
114    pub fn with_subtitle(mut self, subtitle: Subtitle<'a>) -> Self {
115        self.subtitle = Some(subtitle);
116        self
117    }
118
119    /// Set the authors.
120    #[must_use]
121    pub fn with_authors(mut self, authors: Vec<Author<'a>>) -> Self {
122        self.authors = authors;
123        self
124    }
125}
126
127impl<'a> Author<'a> {
128    /// Assemble an author from already-prepared name parts. No normalization
129    /// or allocation happens — callers (tests, external consumers) are
130    /// responsible for providing the display-ready strings they want.
131    #[must_use]
132    pub fn from_parts(
133        first_name: &'a str,
134        middle_name: Option<&'a str>,
135        last_name: &'a str,
136        initials: &'a str,
137    ) -> Self {
138        Self {
139            first_name,
140            middle_name,
141            last_name,
142            initials,
143            email: None,
144        }
145    }
146
147    /// Create a new author with the given names. Arena-allocates any
148    /// underscore-normalized strings and computed initials.
149    #[must_use]
150    pub(crate) fn new(
151        arena: &'a Bump,
152        first_name: &'a str,
153        middle_name: Option<&'a str>,
154        last_name: Option<&'a str>,
155    ) -> Self {
156        let first_processed = first_name.replace('_', " ");
157        let middle_processed = middle_name.map(|m| m.replace('_', " "));
158        let last_processed = last_name.map(|l| l.replace('_', " "));
159
160        let initials = Self::generate_initials(
161            &first_processed,
162            middle_processed.as_deref(),
163            last_processed.as_deref(),
164        );
165
166        Self {
167            first_name: if first_processed == first_name {
168                first_name
169            } else {
170                arena.alloc_str(&first_processed)
171            },
172            middle_name: middle_name
173                .zip(middle_processed.as_ref())
174                .map(|(orig, proc)| {
175                    if proc == orig {
176                        orig
177                    } else {
178                        &*arena.alloc_str(proc)
179                    }
180                }),
181            last_name: last_name
182                .zip(last_processed.as_ref())
183                .map_or("", |(orig, proc)| {
184                    if proc == orig {
185                        orig
186                    } else {
187                        arena.alloc_str(proc)
188                    }
189                }),
190            initials: arena.alloc_str(&initials),
191            email: None,
192        }
193    }
194
195    /// Set the email address.
196    #[must_use]
197    pub fn with_email(mut self, email: &'a str) -> Self {
198        self.email = Some(email);
199        self
200    }
201
202    /// Generate initials from first, optional middle, and last name parts
203    fn generate_initials(first: &str, middle: Option<&str>, last: Option<&str>) -> String {
204        let first_initial = first.chars().next().unwrap_or_default().to_string();
205        let middle_initial = middle
206            .map(|m| m.chars().next().unwrap_or_default().to_string())
207            .unwrap_or_default();
208        let last_initial = last
209            .map(|m| m.chars().next().unwrap_or_default().to_string())
210            .unwrap_or_default();
211        first_initial + &middle_initial + &last_initial
212    }
213}
214
215/// A single-line comment in a document.
216///
217/// Line comments begin with `//` and continue to end of line.
218/// They act as block boundaries but produce no output.
219#[derive(Clone, Debug, PartialEq)]
220#[non_exhaustive]
221pub struct Comment<'a> {
222    pub content: &'a str,
223    pub location: Location,
224}
225
226/// A `Block` represents a block in a document.
227///
228/// A block is a structural element in a document that can contain other blocks.
229#[non_exhaustive]
230#[derive(Clone, Debug, PartialEq, Serialize)]
231#[serde(untagged)]
232pub enum Block<'a> {
233    TableOfContents(TableOfContents<'a>),
234    // TODO(nlopes): we shouldn't have an admonition type here, instead it should be
235    // picked up from the style attribute from the block metadata.
236    //
237    // The main one that would need changing is the Paragraph and the Delimited Example
238    // blocks, where we currently use this but don't need to.
239    Admonition(Admonition<'a>),
240    DiscreteHeader(DiscreteHeader<'a>),
241    DocumentAttribute(DocumentAttribute<'a>),
242    ThematicBreak(ThematicBreak<'a>),
243    PageBreak(PageBreak<'a>),
244    UnorderedList(UnorderedList<'a>),
245    OrderedList(OrderedList<'a>),
246    CalloutList(CalloutList<'a>),
247    DescriptionList(DescriptionList<'a>),
248    Section(Section<'a>),
249    DelimitedBlock(DelimitedBlock<'a>),
250    Paragraph(Paragraph<'a>),
251    Image(Image<'a>),
252    Audio(Audio<'a>),
253    Video(Video<'a>),
254    Comment(Comment<'a>),
255}
256
257impl Locateable for Block<'_> {
258    fn location(&self) -> &Location {
259        match self {
260            Block::Section(s) => &s.location,
261            Block::Paragraph(p) => &p.location,
262            Block::UnorderedList(l) => &l.location,
263            Block::OrderedList(l) => &l.location,
264            Block::DescriptionList(l) => &l.location,
265            Block::CalloutList(l) => &l.location,
266            Block::DelimitedBlock(d) => &d.location,
267            Block::Admonition(a) => &a.location,
268            Block::TableOfContents(t) => &t.location,
269            Block::DiscreteHeader(h) => &h.location,
270            Block::DocumentAttribute(a) => &a.location,
271            Block::ThematicBreak(tb) => &tb.location,
272            Block::PageBreak(pb) => &pb.location,
273            Block::Image(i) => &i.location,
274            Block::Audio(a) => &a.location,
275            Block::Video(v) => &v.location,
276            Block::Comment(c) => &c.location,
277        }
278    }
279}
280
281/// A `DocumentAttribute` represents a document attribute in a document.
282///
283/// A document attribute is a key-value pair that can be used to set metadata in a
284/// document.
285#[derive(Clone, Debug, PartialEq)]
286#[non_exhaustive]
287pub struct DocumentAttribute<'a> {
288    pub name: AttributeName<'a>,
289    pub value: AttributeValue<'a>,
290    pub location: Location,
291}
292
293impl Serialize for DocumentAttribute<'_> {
294    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
295    where
296        S: Serializer,
297    {
298        let mut state = serializer.serialize_map(None)?;
299        state.serialize_entry("name", &self.name)?;
300        state.serialize_entry("type", "attribute")?;
301        state.serialize_entry("value", &self.value)?;
302        state.serialize_entry("location", &self.location)?;
303        state.end()
304    }
305}
306
307/// A `DiscreteHeader` represents a discrete header in a document.
308///
309/// Discrete headings are useful for making headings inside of other blocks, like a
310/// sidebar.
311#[derive(Clone, Debug, PartialEq)]
312#[non_exhaustive]
313pub struct DiscreteHeader<'a> {
314    pub metadata: BlockMetadata<'a>,
315    pub title: Title<'a>,
316    pub level: u8,
317    pub location: Location,
318}
319
320/// A `ThematicBreak` represents a thematic break in a document.
321#[derive(Clone, Default, Debug, PartialEq)]
322#[non_exhaustive]
323pub struct ThematicBreak<'a> {
324    pub anchors: Vec<Anchor<'a>>,
325    pub title: Title<'a>,
326    pub location: Location,
327}
328
329impl Serialize for ThematicBreak<'_> {
330    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
331    where
332        S: Serializer,
333    {
334        let mut state = serializer.serialize_map(None)?;
335        state.serialize_entry("name", "break")?;
336        state.serialize_entry("type", "block")?;
337        state.serialize_entry("variant", "thematic")?;
338        if !self.anchors.is_empty() {
339            state.serialize_entry("anchors", &self.anchors)?;
340        }
341        if !self.title.is_empty() {
342            state.serialize_entry("title", &self.title)?;
343        }
344        state.serialize_entry("location", &self.location)?;
345        state.end()
346    }
347}
348
349/// A `PageBreak` represents a page break in a document.
350#[derive(Clone, Debug, PartialEq)]
351#[non_exhaustive]
352pub struct PageBreak<'a> {
353    pub title: Title<'a>,
354    pub metadata: BlockMetadata<'a>,
355    pub location: Location,
356}
357
358impl Serialize for PageBreak<'_> {
359    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360    where
361        S: Serializer,
362    {
363        let mut state = serializer.serialize_map(None)?;
364        state.serialize_entry("name", "break")?;
365        state.serialize_entry("type", "block")?;
366        state.serialize_entry("variant", "page")?;
367        if !self.title.is_empty() {
368            state.serialize_entry("title", &self.title)?;
369        }
370        if !self.metadata.is_default() {
371            state.serialize_entry("metadata", &self.metadata)?;
372        }
373        state.serialize_entry("location", &self.location)?;
374        state.end()
375    }
376}
377
378impl Serialize for Comment<'_> {
379    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
380    where
381        S: Serializer,
382    {
383        let mut state = serializer.serialize_map(None)?;
384        state.serialize_entry("name", "comment")?;
385        state.serialize_entry("type", "block")?;
386        if !self.content.is_empty() {
387            state.serialize_entry("content", &self.content)?;
388        }
389        state.serialize_entry("location", &self.location)?;
390        state.end()
391    }
392}
393
394/// A `TableOfContents` represents a table of contents block.
395#[derive(Clone, Debug, PartialEq)]
396#[non_exhaustive]
397pub struct TableOfContents<'a> {
398    pub metadata: BlockMetadata<'a>,
399    pub location: Location,
400}
401
402impl Serialize for TableOfContents<'_> {
403    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
404    where
405        S: Serializer,
406    {
407        let mut state = serializer.serialize_map(None)?;
408        state.serialize_entry("name", "toc")?;
409        state.serialize_entry("type", "block")?;
410        if !self.metadata.is_default() {
411            state.serialize_entry("metadata", &self.metadata)?;
412        }
413        state.serialize_entry("location", &self.location)?;
414        state.end()
415    }
416}
417
418/// A `Paragraph` represents a paragraph in a document.
419#[derive(Clone, Debug, PartialEq)]
420#[non_exhaustive]
421pub struct Paragraph<'a> {
422    pub metadata: BlockMetadata<'a>,
423    pub title: Title<'a>,
424    pub content: Vec<InlineNode<'a>>,
425    pub location: Location,
426}
427
428impl<'a> Paragraph<'a> {
429    /// Create a new paragraph with the given content and location.
430    #[must_use]
431    pub fn new(content: Vec<InlineNode<'a>>, location: Location) -> Self {
432        Self {
433            metadata: BlockMetadata::default(),
434            title: Title::default(),
435            content,
436            location,
437        }
438    }
439
440    /// Set the metadata.
441    #[must_use]
442    pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
443        self.metadata = metadata;
444        self
445    }
446
447    /// Set the title.
448    #[must_use]
449    pub fn with_title(mut self, title: Title<'a>) -> Self {
450        self.title = title;
451        self
452    }
453}
454
455/// A `DelimitedBlock` represents a delimited block in a document.
456#[derive(Clone, Debug, PartialEq)]
457#[non_exhaustive]
458pub struct DelimitedBlock<'a> {
459    pub metadata: BlockMetadata<'a>,
460    pub inner: DelimitedBlockType<'a>,
461    pub delimiter: &'a str,
462    pub title: Title<'a>,
463    pub location: Location,
464    pub open_delimiter_location: Option<Location>,
465    pub close_delimiter_location: Option<Location>,
466}
467
468impl<'a> DelimitedBlock<'a> {
469    /// Create a new delimited block.
470    #[must_use]
471    pub fn new(inner: DelimitedBlockType<'a>, delimiter: &'a str, location: Location) -> Self {
472        Self {
473            metadata: BlockMetadata::default(),
474            inner,
475            delimiter,
476            title: Title::default(),
477            location,
478            open_delimiter_location: None,
479            close_delimiter_location: None,
480        }
481    }
482
483    /// Set the metadata.
484    #[must_use]
485    pub fn with_metadata(mut self, metadata: BlockMetadata<'a>) -> Self {
486        self.metadata = metadata;
487        self
488    }
489
490    /// Set the title.
491    #[must_use]
492    pub fn with_title(mut self, title: Title<'a>) -> Self {
493        self.title = title;
494        self
495    }
496}
497
498/// Notation type for mathematical expressions.
499#[derive(Clone, Debug, PartialEq, Serialize)]
500#[serde(rename_all = "lowercase")]
501pub enum StemNotation {
502    Latexmath,
503    Asciimath,
504}
505
506impl Display for StemNotation {
507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508        match self {
509            StemNotation::Latexmath => write!(f, "latexmath"),
510            StemNotation::Asciimath => write!(f, "asciimath"),
511        }
512    }
513}
514
515impl FromStr for StemNotation {
516    type Err = String;
517
518    fn from_str(s: &str) -> Result<Self, Self::Err> {
519        match s {
520            "latexmath" => Ok(Self::Latexmath),
521            "asciimath" => Ok(Self::Asciimath),
522            _ => Err(format!("unknown stem notation: {s}")),
523        }
524    }
525}
526
527/// Content of a stem block with math notation.
528#[derive(Clone, Debug, PartialEq, Serialize)]
529#[non_exhaustive]
530pub struct StemContent<'a> {
531    pub content: &'a str,
532    pub notation: StemNotation,
533}
534
535impl<'a> StemContent<'a> {
536    /// Create a new stem content with the given content and notation.
537    #[must_use]
538    pub fn new(content: &'a str, notation: StemNotation) -> Self {
539        Self { content, notation }
540    }
541}
542
543/// The inner content type of a delimited block.
544///
545/// Each variant wraps the content appropriate for that block type:
546/// - **Verbatim content** (`Vec<InlineNode>`): `DelimitedListing`, `DelimitedLiteral`,
547///   `DelimitedPass`, `DelimitedVerse`, `DelimitedComment` - preserves whitespace/formatting
548/// - **Compound content** (`Vec<Block>`): `DelimitedExample`, `DelimitedOpen`,
549///   `DelimitedSidebar`, `DelimitedQuote` - can contain nested blocks
550/// - **Structured content**: `DelimitedTable(Table)`, `DelimitedStem(StemContent)`
551///
552/// # Accessing Content
553///
554/// Use pattern matching to extract the inner content:
555///
556/// ```
557/// # use acdc_parser::{DelimitedBlockType, Block, InlineNode};
558/// fn process_block(block_type: &DelimitedBlockType) {
559///     match block_type {
560///         DelimitedBlockType::DelimitedListing(inlines) => {
561///             // Handle listing content (source code, etc.)
562///         }
563///         DelimitedBlockType::DelimitedExample(blocks) => {
564///             // Handle example with nested blocks
565///         }
566///         DelimitedBlockType::DelimitedTable(table) => {
567///             // Access table.rows, table.header, etc.
568///         }
569///         // ... other variants
570///         _ => {}
571///     }
572/// }
573/// ```
574///
575/// # Note on Variant Names
576///
577/// Variants are prefixed with `Delimited` to disambiguate from potential future
578/// non-delimited block types and to make pattern matching more explicit.
579#[non_exhaustive]
580#[derive(Clone, Debug, PartialEq, Serialize)]
581#[serde(untagged)]
582pub enum DelimitedBlockType<'a> {
583    /// Comment block content (not rendered in output).
584    DelimitedComment(Vec<InlineNode<'a>>),
585    /// Example block - can contain nested blocks, admonitions, etc.
586    DelimitedExample(Vec<Block<'a>>),
587    /// Listing block - typically source code with syntax highlighting.
588    DelimitedListing(Vec<InlineNode<'a>>),
589    /// Literal block - preformatted text rendered verbatim.
590    DelimitedLiteral(Vec<InlineNode<'a>>),
591    /// Open block - generic container for nested blocks.
592    DelimitedOpen(Vec<Block<'a>>),
593    /// Sidebar block - supplementary content in a styled container.
594    DelimitedSidebar(Vec<Block<'a>>),
595    /// Table block - structured tabular data.
596    DelimitedTable(Table<'a>),
597    /// Passthrough block - content passed directly to output without processing.
598    DelimitedPass(Vec<InlineNode<'a>>),
599    /// Quote block - blockquote with optional attribution.
600    DelimitedQuote(Vec<Block<'a>>),
601    /// Verse block - poetry/lyrics preserving line breaks.
602    DelimitedVerse(Vec<InlineNode<'a>>),
603    /// STEM (math) block - LaTeX or `AsciiMath` notation.
604    DelimitedStem(StemContent<'a>),
605}
606
607impl DelimitedBlockType<'_> {
608    fn name(&self) -> &'static str {
609        match self {
610            DelimitedBlockType::DelimitedComment(_) => "comment",
611            DelimitedBlockType::DelimitedExample(_) => "example",
612            DelimitedBlockType::DelimitedListing(_) => "listing",
613            DelimitedBlockType::DelimitedLiteral(_) => "literal",
614            DelimitedBlockType::DelimitedOpen(_) => "open",
615            DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
616            DelimitedBlockType::DelimitedTable(_) => "table",
617            DelimitedBlockType::DelimitedPass(_) => "pass",
618            DelimitedBlockType::DelimitedQuote(_) => "quote",
619            DelimitedBlockType::DelimitedVerse(_) => "verse",
620            DelimitedBlockType::DelimitedStem(_) => "stem",
621        }
622    }
623}
624
625impl Serialize for Document<'_> {
626    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
627    where
628        S: Serializer,
629    {
630        let mut state = serializer.serialize_map(None)?;
631        state.serialize_entry("name", "document")?;
632        state.serialize_entry("type", "block")?;
633        if let Some(header) = &self.header {
634            state.serialize_entry("header", header)?;
635            // We serialize the attributes even if they're empty because that's what the
636            // TCK expects (odd but true)
637            state.serialize_entry("attributes", &self.attributes)?;
638        } else if !self.attributes.is_empty() {
639            state.serialize_entry("attributes", &self.attributes)?;
640        }
641        if !self.blocks.is_empty() {
642            state.serialize_entry("blocks", &self.blocks)?;
643        }
644        state.serialize_entry("location", &self.location)?;
645        state.end()
646    }
647}
648
649impl Serialize for DelimitedBlock<'_> {
650    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
651    where
652        S: Serializer,
653    {
654        let mut state = serializer.serialize_map(None)?;
655        state.serialize_entry("name", self.inner.name())?;
656        state.serialize_entry("type", "block")?;
657        state.serialize_entry("form", "delimited")?;
658        state.serialize_entry("delimiter", &self.delimiter)?;
659        if !self.metadata.is_default() {
660            state.serialize_entry("metadata", &self.metadata)?;
661        }
662
663        match &self.inner {
664            DelimitedBlockType::DelimitedStem(stem) => {
665                state.serialize_entry("content", &stem.content)?;
666                state.serialize_entry("notation", &stem.notation)?;
667            }
668            DelimitedBlockType::DelimitedListing(inner)
669            | DelimitedBlockType::DelimitedLiteral(inner)
670            | DelimitedBlockType::DelimitedPass(inner)
671            | DelimitedBlockType::DelimitedVerse(inner) => {
672                state.serialize_entry("inlines", &inner)?;
673            }
674            DelimitedBlockType::DelimitedTable(inner) => {
675                state.serialize_entry("content", &inner)?;
676            }
677            inner @ (DelimitedBlockType::DelimitedComment(_)
678            | DelimitedBlockType::DelimitedExample(_)
679            | DelimitedBlockType::DelimitedOpen(_)
680            | DelimitedBlockType::DelimitedQuote(_)
681            | DelimitedBlockType::DelimitedSidebar(_)) => {
682                state.serialize_entry("blocks", &inner)?;
683            }
684        }
685        if !self.title.is_empty() {
686            state.serialize_entry("title", &self.title)?;
687        }
688        state.serialize_entry("location", &self.location)?;
689        state.end()
690    }
691}
692
693impl Serialize for DiscreteHeader<'_> {
694    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
695    where
696        S: Serializer,
697    {
698        let mut state = serializer.serialize_map(None)?;
699        state.serialize_entry("name", "heading")?;
700        state.serialize_entry("type", "block")?;
701        if !self.title.is_empty() {
702            state.serialize_entry("title", &self.title)?;
703        }
704        state.serialize_entry("level", &self.level)?;
705        if !self.metadata.is_default() {
706            state.serialize_entry("metadata", &self.metadata)?;
707        }
708        state.serialize_entry("location", &self.location)?;
709        state.end()
710    }
711}
712
713impl Serialize for Paragraph<'_> {
714    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
715    where
716        S: Serializer,
717    {
718        let mut state = serializer.serialize_map(None)?;
719        state.serialize_entry("name", "paragraph")?;
720        state.serialize_entry("type", "block")?;
721        if !self.title.is_empty() {
722            state.serialize_entry("title", &self.title)?;
723        }
724        state.serialize_entry("inlines", &self.content)?;
725        if !self.metadata.is_default() {
726            state.serialize_entry("metadata", &self.metadata)?;
727        }
728        state.serialize_entry("location", &self.location)?;
729        state.end()
730    }
731}