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