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    /// Location of the delimiter in the source.
87    #[serde(skip)]
88    pub delimiter_location: Option<Location>,
89    /// Inline content immediately after the delimiter on the same line.
90    #[serde(default, skip_serializing_if = "Vec::is_empty")]
91    pub principal_text: Vec<InlineNode>,
92    /// Block content providing the description (singular, not plural).
93    pub description: Vec<Block>,
94    pub location: Location,
95}
96
97/// A `UnorderedList` represents an unordered list in a document.
98#[derive(Clone, Debug, PartialEq)]
99#[non_exhaustive]
100pub struct UnorderedList {
101    pub title: Title,
102    pub metadata: BlockMetadata,
103    pub items: Vec<ListItem>,
104    pub marker: String,
105    pub location: Location,
106}
107
108/// An `OrderedList` represents an ordered list in a document.
109#[derive(Clone, Debug, PartialEq)]
110#[non_exhaustive]
111pub struct OrderedList {
112    pub title: Title,
113    pub metadata: BlockMetadata,
114    pub items: Vec<ListItem>,
115    pub marker: String,
116    pub location: Location,
117}
118
119/// A `CalloutList` represents a callout list in a document.
120///
121/// Callout lists are used to annotate code blocks with numbered references.
122#[derive(Clone, Debug, PartialEq)]
123#[non_exhaustive]
124pub struct CalloutList {
125    pub title: Title,
126    pub metadata: BlockMetadata,
127    pub items: Vec<CalloutListItem>,
128    pub location: Location,
129}
130
131/// A `CalloutListItem` represents an item in a callout list.
132///
133/// Unlike [`ListItem`], callout list items have a structured [`CalloutRef`] that
134/// preserves whether the original marker was explicit (`<1>`) or auto-numbered (`<.>`).
135///
136/// # Example
137///
138/// ```asciidoc
139/// <1> First explanation
140/// <.> Auto-numbered explanation
141/// ```
142#[derive(Clone, Debug, PartialEq)]
143#[non_exhaustive]
144pub struct CalloutListItem {
145    /// The callout reference (explicit or auto-numbered).
146    pub callout: CalloutRef,
147    /// Principal text - inline content that appears after the callout marker.
148    pub principal: Vec<InlineNode>,
149    /// Attached blocks - blocks attached via continuation (though rarely used for callouts).
150    pub blocks: Vec<Block>,
151    /// Source location of this item.
152    pub location: Location,
153}
154
155// =============================================================================
156// Serialization
157// =============================================================================
158
159macro_rules! impl_list_serialize {
160    ($type:ty, $variant:literal, with_marker) => {
161        impl Serialize for $type {
162            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
163            where
164                S: Serializer,
165            {
166                let mut state = serializer.serialize_map(None)?;
167                state.serialize_entry("name", "list")?;
168                state.serialize_entry("type", "block")?;
169                state.serialize_entry("variant", $variant)?;
170                state.serialize_entry("marker", &self.marker)?;
171                if !self.title.is_empty() {
172                    state.serialize_entry("title", &self.title)?;
173                }
174                if !self.metadata.is_default() {
175                    state.serialize_entry("metadata", &self.metadata)?;
176                }
177                state.serialize_entry("items", &self.items)?;
178                state.serialize_entry("location", &self.location)?;
179                state.end()
180            }
181        }
182    };
183    ($type:ty, $variant:literal) => {
184        impl Serialize for $type {
185            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
186            where
187                S: Serializer,
188            {
189                let mut state = serializer.serialize_map(None)?;
190                state.serialize_entry("name", "list")?;
191                state.serialize_entry("type", "block")?;
192                state.serialize_entry("variant", $variant)?;
193                if !self.title.is_empty() {
194                    state.serialize_entry("title", &self.title)?;
195                }
196                if !self.metadata.is_default() {
197                    state.serialize_entry("metadata", &self.metadata)?;
198                }
199                state.serialize_entry("items", &self.items)?;
200                state.serialize_entry("location", &self.location)?;
201                state.end()
202            }
203        }
204    };
205}
206
207impl_list_serialize!(UnorderedList, "unordered", with_marker);
208impl_list_serialize!(OrderedList, "ordered", with_marker);
209impl_list_serialize!(CalloutList, "callout");
210
211impl Serialize for DescriptionList {
212    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213    where
214        S: Serializer,
215    {
216        let mut state = serializer.serialize_map(None)?;
217        state.serialize_entry("name", "dlist")?;
218        state.serialize_entry("type", "block")?;
219        if !self.title.is_empty() {
220            state.serialize_entry("title", &self.title)?;
221        }
222        if !self.metadata.is_default() {
223            state.serialize_entry("metadata", &self.metadata)?;
224        }
225        state.serialize_entry("items", &self.items)?;
226        state.serialize_entry("location", &self.location)?;
227        state.end()
228    }
229}
230
231impl Serialize for ListItem {
232    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
233    where
234        S: Serializer,
235    {
236        let mut state = serializer.serialize_map(None)?;
237        state.serialize_entry("name", "listItem")?;
238        state.serialize_entry("type", "block")?;
239        state.serialize_entry("marker", &self.marker)?;
240        if let Some(checked) = &self.checked {
241            state.serialize_entry("checked", checked)?;
242        }
243        // The TCK doesn't contain level information for list items, so we don't serialize
244        // it.
245        //
246        // Uncomment the line below if level information is added in the future.
247        //
248        // state.serialize_entry("level", &self.level)?;
249        state.serialize_entry("principal", &self.principal)?;
250        if !self.blocks.is_empty() {
251            state.serialize_entry("blocks", &self.blocks)?;
252        }
253        state.serialize_entry("location", &self.location)?;
254        state.end()
255    }
256}
257
258impl Serialize for ListItemCheckedStatus {
259    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
260    where
261        S: Serializer,
262    {
263        match &self {
264            ListItemCheckedStatus::Checked => serializer.serialize_bool(true),
265            ListItemCheckedStatus::Unchecked => serializer.serialize_bool(false),
266        }
267    }
268}
269
270impl Serialize for CalloutListItem {
271    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
272    where
273        S: Serializer,
274    {
275        let mut state = serializer.serialize_map(None)?;
276        state.serialize_entry("name", "listItem")?;
277        state.serialize_entry("type", "block")?;
278        state.serialize_entry("callout", &self.callout)?;
279        state.serialize_entry("principal", &self.principal)?;
280        if !self.blocks.is_empty() {
281            state.serialize_entry("blocks", &self.blocks)?;
282        }
283        state.serialize_entry("location", &self.location)?;
284        state.end()
285    }
286}