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