Skip to main content

acdc_parser/model/
lists.rs

1//! List types for `AsciiDoc` documents.
2
3use serde::{
4    Serialize,
5    ser::{SerializeMap, Serializer},
6};
7
8use super::Block;
9use super::anchor::Anchor;
10use super::inlines::{CalloutRef, InlineNode};
11use super::location::Location;
12use super::metadata::BlockMetadata;
13use super::title::Title;
14
15pub type ListLevel = u8;
16
17/// A `ListItemCheckedStatus` represents the checked status of a list item.
18#[derive(Clone, Debug, PartialEq)]
19#[non_exhaustive]
20pub enum ListItemCheckedStatus {
21    Checked,
22    Unchecked,
23}
24
25/// A `ListItem` represents a list item in a document.
26///
27/// List items have principal text (inline content immediately after the marker) and
28/// optionally attached blocks (via continuation or nesting). This matches Asciidoctor's
29/// AST structure where principal text renders as bare `<p>` and attached blocks render
30/// with their full wrapper divs.
31#[derive(Clone, Debug, PartialEq)]
32#[non_exhaustive]
33pub struct ListItem {
34    pub level: ListLevel,
35    pub marker: String,
36    pub checked: Option<ListItemCheckedStatus>,
37    /// Principal text - inline content that appears immediately after the list marker
38    pub principal: Vec<InlineNode>,
39    /// Attached blocks - blocks attached via continuation (+) or nesting
40    pub blocks: Vec<Block>,
41    pub location: Location,
42}
43
44/// A `DescriptionList` represents a description list in a document.
45#[derive(Clone, Debug, PartialEq)]
46#[non_exhaustive]
47pub struct DescriptionList {
48    pub title: Title,
49    pub metadata: BlockMetadata,
50    pub items: Vec<DescriptionListItem>,
51    pub location: Location,
52}
53
54/// An item in a description list (term + description).
55///
56/// # Structure
57///
58/// ```text
59/// term:: principal text    <- term, delimiter, principal_text
60///        description       <- description (blocks)
61/// ```
62///
63/// # Note on Field Names
64///
65/// - `description` is **singular** (not `descriptions`) - it holds the block content
66///   following the term
67/// - `principal_text` is inline content immediately after the delimiter on the same line
68///
69/// ```
70/// # use acdc_parser::DescriptionListItem;
71/// fn has_description(item: &DescriptionListItem) -> bool {
72///     !item.description.is_empty()  // Note: singular 'description'
73/// }
74/// ```
75#[derive(Clone, Debug, PartialEq, Serialize)]
76#[non_exhaustive]
77pub struct DescriptionListItem {
78    /// Optional anchors (IDs) attached to this item.
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub anchors: Vec<Anchor>,
81    /// The term being defined (inline content before the delimiter).
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub term: Vec<InlineNode>,
84    /// The delimiter used (`::`, `:::`, `::::`, or `;;`).
85    pub delimiter: String,
86    /// Inline content immediately after the delimiter on the same line.
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    pub principal_text: Vec<InlineNode>,
89    /// Block content providing the description (singular, not plural).
90    pub description: Vec<Block>,
91    pub location: Location,
92}
93
94/// A `UnorderedList` represents an unordered list in a document.
95#[derive(Clone, Debug, PartialEq)]
96#[non_exhaustive]
97pub struct UnorderedList {
98    pub title: Title,
99    pub metadata: BlockMetadata,
100    pub items: Vec<ListItem>,
101    pub marker: String,
102    pub location: Location,
103}
104
105/// An `OrderedList` represents an ordered list in a document.
106#[derive(Clone, Debug, PartialEq)]
107#[non_exhaustive]
108pub struct OrderedList {
109    pub title: Title,
110    pub metadata: BlockMetadata,
111    pub items: Vec<ListItem>,
112    pub marker: String,
113    pub location: Location,
114}
115
116/// A `CalloutList` represents a callout list in a document.
117///
118/// Callout lists are used to annotate code blocks with numbered references.
119#[derive(Clone, Debug, PartialEq)]
120#[non_exhaustive]
121pub struct CalloutList {
122    pub title: Title,
123    pub metadata: BlockMetadata,
124    pub items: Vec<CalloutListItem>,
125    pub location: Location,
126}
127
128/// A `CalloutListItem` represents an item in a callout list.
129///
130/// Unlike [`ListItem`], callout list items have a structured [`CalloutRef`] that
131/// preserves whether the original marker was explicit (`<1>`) or auto-numbered (`<.>`).
132///
133/// # Example
134///
135/// ```asciidoc
136/// <1> First explanation
137/// <.> Auto-numbered explanation
138/// ```
139#[derive(Clone, Debug, PartialEq)]
140#[non_exhaustive]
141pub struct CalloutListItem {
142    /// The callout reference (explicit or auto-numbered).
143    pub callout: CalloutRef,
144    /// Principal text - inline content that appears after the callout marker.
145    pub principal: Vec<InlineNode>,
146    /// Attached blocks - blocks attached via continuation (though rarely used for callouts).
147    pub blocks: Vec<Block>,
148    /// Source location of this item.
149    pub location: Location,
150}
151
152// =============================================================================
153// Serialization
154// =============================================================================
155
156macro_rules! impl_list_serialize {
157    ($type:ty, $variant:literal, with_marker) => {
158        impl Serialize for $type {
159            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160            where
161                S: Serializer,
162            {
163                let mut state = serializer.serialize_map(None)?;
164                state.serialize_entry("name", "list")?;
165                state.serialize_entry("type", "block")?;
166                state.serialize_entry("variant", $variant)?;
167                state.serialize_entry("marker", &self.marker)?;
168                if !self.title.is_empty() {
169                    state.serialize_entry("title", &self.title)?;
170                }
171                if !self.metadata.is_default() {
172                    state.serialize_entry("metadata", &self.metadata)?;
173                }
174                state.serialize_entry("items", &self.items)?;
175                state.serialize_entry("location", &self.location)?;
176                state.end()
177            }
178        }
179    };
180    ($type:ty, $variant:literal) => {
181        impl Serialize for $type {
182            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183            where
184                S: Serializer,
185            {
186                let mut state = serializer.serialize_map(None)?;
187                state.serialize_entry("name", "list")?;
188                state.serialize_entry("type", "block")?;
189                state.serialize_entry("variant", $variant)?;
190                if !self.title.is_empty() {
191                    state.serialize_entry("title", &self.title)?;
192                }
193                if !self.metadata.is_default() {
194                    state.serialize_entry("metadata", &self.metadata)?;
195                }
196                state.serialize_entry("items", &self.items)?;
197                state.serialize_entry("location", &self.location)?;
198                state.end()
199            }
200        }
201    };
202}
203
204impl_list_serialize!(UnorderedList, "unordered", with_marker);
205impl_list_serialize!(OrderedList, "ordered", with_marker);
206impl_list_serialize!(CalloutList, "callout");
207
208impl Serialize for DescriptionList {
209    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210    where
211        S: Serializer,
212    {
213        let mut state = serializer.serialize_map(None)?;
214        state.serialize_entry("name", "dlist")?;
215        state.serialize_entry("type", "block")?;
216        if !self.title.is_empty() {
217            state.serialize_entry("title", &self.title)?;
218        }
219        if !self.metadata.is_default() {
220            state.serialize_entry("metadata", &self.metadata)?;
221        }
222        state.serialize_entry("items", &self.items)?;
223        state.serialize_entry("location", &self.location)?;
224        state.end()
225    }
226}
227
228impl Serialize for ListItem {
229    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230    where
231        S: Serializer,
232    {
233        let mut state = serializer.serialize_map(None)?;
234        state.serialize_entry("name", "listItem")?;
235        state.serialize_entry("type", "block")?;
236        state.serialize_entry("marker", &self.marker)?;
237        if let Some(checked) = &self.checked {
238            state.serialize_entry("checked", checked)?;
239        }
240        // The TCK doesn't contain level information for list items, so we don't serialize
241        // it.
242        //
243        // Uncomment the line below if level information is added in the future.
244        //
245        // state.serialize_entry("level", &self.level)?;
246        state.serialize_entry("principal", &self.principal)?;
247        if !self.blocks.is_empty() {
248            state.serialize_entry("blocks", &self.blocks)?;
249        }
250        state.serialize_entry("location", &self.location)?;
251        state.end()
252    }
253}
254
255impl Serialize for ListItemCheckedStatus {
256    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
257    where
258        S: Serializer,
259    {
260        match &self {
261            ListItemCheckedStatus::Checked => serializer.serialize_bool(true),
262            ListItemCheckedStatus::Unchecked => serializer.serialize_bool(false),
263        }
264    }
265}
266
267impl Serialize for CalloutListItem {
268    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
269    where
270        S: Serializer,
271    {
272        let mut state = serializer.serialize_map(None)?;
273        state.serialize_entry("name", "listItem")?;
274        state.serialize_entry("type", "block")?;
275        state.serialize_entry("callout", &self.callout)?;
276        state.serialize_entry("principal", &self.principal)?;
277        if !self.blocks.is_empty() {
278            state.serialize_entry("blocks", &self.blocks)?;
279        }
280        state.serialize_entry("location", &self.location)?;
281        state.end()
282    }
283}