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}
413
414impl DelimitedBlock {
415    /// Create a new delimited block.
416    #[must_use]
417    pub fn new(inner: DelimitedBlockType, delimiter: String, location: Location) -> Self {
418        Self {
419            metadata: BlockMetadata::default(),
420            inner,
421            delimiter,
422            title: Title::default(),
423            location,
424        }
425    }
426
427    /// Set the metadata.
428    #[must_use]
429    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
430        self.metadata = metadata;
431        self
432    }
433
434    /// Set the title.
435    #[must_use]
436    pub fn with_title(mut self, title: Title) -> Self {
437        self.title = title;
438        self
439    }
440}
441
442/// Notation type for mathematical expressions.
443#[derive(Clone, Debug, PartialEq, Serialize)]
444#[serde(rename_all = "lowercase")]
445pub enum StemNotation {
446    Latexmath,
447    Asciimath,
448}
449
450impl Display for StemNotation {
451    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452        match self {
453            StemNotation::Latexmath => write!(f, "latexmath"),
454            StemNotation::Asciimath => write!(f, "asciimath"),
455        }
456    }
457}
458
459impl FromStr for StemNotation {
460    type Err = String;
461
462    fn from_str(s: &str) -> Result<Self, Self::Err> {
463        match s {
464            "latexmath" => Ok(Self::Latexmath),
465            "asciimath" => Ok(Self::Asciimath),
466            _ => Err(format!("unknown stem notation: {s}")),
467        }
468    }
469}
470
471/// Content of a stem block with math notation.
472#[derive(Clone, Debug, PartialEq, Serialize)]
473#[non_exhaustive]
474pub struct StemContent {
475    pub content: String,
476    pub notation: StemNotation,
477}
478
479impl StemContent {
480    /// Create a new stem content with the given content and notation.
481    #[must_use]
482    pub fn new(content: String, notation: StemNotation) -> Self {
483        Self { content, notation }
484    }
485}
486
487/// The inner content type of a delimited block.
488///
489/// Each variant wraps the content appropriate for that block type:
490/// - **Verbatim content** (`Vec<InlineNode>`): `DelimitedListing`, `DelimitedLiteral`,
491///   `DelimitedPass`, `DelimitedVerse`, `DelimitedComment` - preserves whitespace/formatting
492/// - **Compound content** (`Vec<Block>`): `DelimitedExample`, `DelimitedOpen`,
493///   `DelimitedSidebar`, `DelimitedQuote` - can contain nested blocks
494/// - **Structured content**: `DelimitedTable(Table)`, `DelimitedStem(StemContent)`
495///
496/// # Accessing Content
497///
498/// Use pattern matching to extract the inner content:
499///
500/// ```
501/// # use acdc_parser::{DelimitedBlockType, Block, InlineNode};
502/// fn process_block(block_type: &DelimitedBlockType) {
503///     match block_type {
504///         DelimitedBlockType::DelimitedListing(inlines) => {
505///             // Handle listing content (source code, etc.)
506///         }
507///         DelimitedBlockType::DelimitedExample(blocks) => {
508///             // Handle example with nested blocks
509///         }
510///         DelimitedBlockType::DelimitedTable(table) => {
511///             // Access table.rows, table.header, etc.
512///         }
513///         // ... other variants
514///         _ => {}
515///     }
516/// }
517/// ```
518///
519/// # Note on Variant Names
520///
521/// Variants are prefixed with `Delimited` to disambiguate from potential future
522/// non-delimited block types and to make pattern matching more explicit.
523#[non_exhaustive]
524#[derive(Clone, Debug, PartialEq, Serialize)]
525#[serde(untagged)]
526pub enum DelimitedBlockType {
527    /// Comment block content (not rendered in output).
528    DelimitedComment(Vec<InlineNode>),
529    /// Example block - can contain nested blocks, admonitions, etc.
530    DelimitedExample(Vec<Block>),
531    /// Listing block - typically source code with syntax highlighting.
532    DelimitedListing(Vec<InlineNode>),
533    /// Literal block - preformatted text rendered verbatim.
534    DelimitedLiteral(Vec<InlineNode>),
535    /// Open block - generic container for nested blocks.
536    DelimitedOpen(Vec<Block>),
537    /// Sidebar block - supplementary content in a styled container.
538    DelimitedSidebar(Vec<Block>),
539    /// Table block - structured tabular data.
540    DelimitedTable(Table),
541    /// Passthrough block - content passed directly to output without processing.
542    DelimitedPass(Vec<InlineNode>),
543    /// Quote block - blockquote with optional attribution.
544    DelimitedQuote(Vec<Block>),
545    /// Verse block - poetry/lyrics preserving line breaks.
546    DelimitedVerse(Vec<InlineNode>),
547    /// STEM (math) block - LaTeX or `AsciiMath` notation.
548    DelimitedStem(StemContent),
549}
550
551impl DelimitedBlockType {
552    fn name(&self) -> &'static str {
553        match self {
554            DelimitedBlockType::DelimitedComment(_) => "comment",
555            DelimitedBlockType::DelimitedExample(_) => "example",
556            DelimitedBlockType::DelimitedListing(_) => "listing",
557            DelimitedBlockType::DelimitedLiteral(_) => "literal",
558            DelimitedBlockType::DelimitedOpen(_) => "open",
559            DelimitedBlockType::DelimitedSidebar(_) => "sidebar",
560            DelimitedBlockType::DelimitedTable(_) => "table",
561            DelimitedBlockType::DelimitedPass(_) => "pass",
562            DelimitedBlockType::DelimitedQuote(_) => "quote",
563            DelimitedBlockType::DelimitedVerse(_) => "verse",
564            DelimitedBlockType::DelimitedStem(_) => "stem",
565        }
566    }
567}
568
569impl Serialize for Document {
570    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
571    where
572        S: Serializer,
573    {
574        let mut state = serializer.serialize_map(None)?;
575        state.serialize_entry("name", "document")?;
576        state.serialize_entry("type", "block")?;
577        if let Some(header) = &self.header {
578            state.serialize_entry("header", header)?;
579            // We serialize the attributes even if they're empty because that's what the
580            // TCK expects (odd but true)
581            state.serialize_entry("attributes", &self.attributes)?;
582        } else if !self.attributes.is_empty() {
583            state.serialize_entry("attributes", &self.attributes)?;
584        }
585        if !self.blocks.is_empty() {
586            state.serialize_entry("blocks", &self.blocks)?;
587        }
588        state.serialize_entry("location", &self.location)?;
589        state.end()
590    }
591}
592
593impl Serialize for DelimitedBlock {
594    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
595    where
596        S: Serializer,
597    {
598        let mut state = serializer.serialize_map(None)?;
599        state.serialize_entry("name", self.inner.name())?;
600        state.serialize_entry("type", "block")?;
601        state.serialize_entry("form", "delimited")?;
602        state.serialize_entry("delimiter", &self.delimiter)?;
603        if !self.metadata.is_default() {
604            state.serialize_entry("metadata", &self.metadata)?;
605        }
606
607        match &self.inner {
608            DelimitedBlockType::DelimitedStem(stem) => {
609                state.serialize_entry("content", &stem.content)?;
610                state.serialize_entry("notation", &stem.notation)?;
611            }
612            DelimitedBlockType::DelimitedListing(inner)
613            | DelimitedBlockType::DelimitedLiteral(inner)
614            | DelimitedBlockType::DelimitedPass(inner)
615            | DelimitedBlockType::DelimitedVerse(inner) => {
616                state.serialize_entry("inlines", &inner)?;
617            }
618            DelimitedBlockType::DelimitedTable(inner) => {
619                state.serialize_entry("content", &inner)?;
620            }
621            inner @ (DelimitedBlockType::DelimitedComment(_)
622            | DelimitedBlockType::DelimitedExample(_)
623            | DelimitedBlockType::DelimitedOpen(_)
624            | DelimitedBlockType::DelimitedQuote(_)
625            | DelimitedBlockType::DelimitedSidebar(_)) => {
626                state.serialize_entry("blocks", &inner)?;
627            }
628        }
629        if !self.title.is_empty() {
630            state.serialize_entry("title", &self.title)?;
631        }
632        state.serialize_entry("location", &self.location)?;
633        state.end()
634    }
635}
636
637impl Serialize for DiscreteHeader {
638    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
639    where
640        S: Serializer,
641    {
642        let mut state = serializer.serialize_map(None)?;
643        state.serialize_entry("name", "heading")?;
644        state.serialize_entry("type", "block")?;
645        if !self.title.is_empty() {
646            state.serialize_entry("title", &self.title)?;
647        }
648        state.serialize_entry("level", &self.level)?;
649        if !self.metadata.is_default() {
650            state.serialize_entry("metadata", &self.metadata)?;
651        }
652        state.serialize_entry("location", &self.location)?;
653        state.end()
654    }
655}
656
657impl Serialize for Paragraph {
658    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
659    where
660        S: Serializer,
661    {
662        let mut state = serializer.serialize_map(None)?;
663        state.serialize_entry("name", "paragraph")?;
664        state.serialize_entry("type", "block")?;
665        if !self.title.is_empty() {
666            state.serialize_entry("title", &self.title)?;
667        }
668        state.serialize_entry("inlines", &self.content)?;
669        if !self.metadata.is_default() {
670            state.serialize_entry("metadata", &self.metadata)?;
671        }
672        state.serialize_entry("location", &self.location)?;
673        state.end()
674    }
675}