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