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<'a> {
34    pub level: ListLevel,
35    pub marker: &'a str,
36    pub checked: Option<ListItemCheckedStatus>,
37    /// Principal text - inline content that appears immediately after the list marker
38    pub principal: Vec<InlineNode<'a>>,
39    /// Attached blocks - blocks attached via continuation (+) or nesting
40    pub blocks: Vec<Block<'a>>,
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<'a> {
48    pub title: Title<'a>,
49    pub metadata: BlockMetadata<'a>,
50    pub items: Vec<DescriptionListItem<'a>>,
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<'a> {
78    /// Optional anchors (IDs) attached to this item.
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub anchors: Vec<Anchor<'a>>,
81    /// The term being defined (inline content before the delimiter).
82    #[serde(default, skip_serializing_if = "Vec::is_empty")]
83    pub term: Vec<InlineNode<'a>>,
84    /// The delimiter used (`::`, `:::`, `::::`, or `;;`).
85    pub delimiter: &'a str,
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<'a>>,
92    /// Block content providing the description (singular, not plural).
93    pub description: Vec<Block<'a>>,
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<'a> {
101    pub title: Title<'a>,
102    pub metadata: BlockMetadata<'a>,
103    pub items: Vec<ListItem<'a>>,
104    pub marker: &'a str,
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<'a> {
112    pub title: Title<'a>,
113    pub metadata: BlockMetadata<'a>,
114    pub items: Vec<ListItem<'a>>,
115    pub marker: &'a str,
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<'a> {
125    pub title: Title<'a>,
126    pub metadata: BlockMetadata<'a>,
127    pub items: Vec<CalloutListItem<'a>>,
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<'a> {
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<'a>>,
149    /// Attached blocks - blocks attached via continuation (though rarely used for callouts).
150    pub blocks: Vec<Block<'a>>,
151    /// Source location of this item.
152    pub location: Location,
153}
154
155// =============================================================================
156// Serialization
157// =============================================================================
158
159impl Serialize for UnorderedList<'_> {
160    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
161    where
162        S: Serializer,
163    {
164        let mut state = serializer.serialize_map(None)?;
165        state.serialize_entry("name", "list")?;
166        state.serialize_entry("type", "block")?;
167        state.serialize_entry("variant", "unordered")?;
168        state.serialize_entry("marker", &self.marker)?;
169        if !self.title.is_empty() {
170            state.serialize_entry("title", &self.title)?;
171        }
172        if !self.metadata.is_default() {
173            state.serialize_entry("metadata", &self.metadata)?;
174        }
175        state.serialize_entry("items", &self.items)?;
176        state.serialize_entry("location", &self.location)?;
177        state.end()
178    }
179}
180
181impl Serialize for OrderedList<'_> {
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", "ordered")?;
190        state.serialize_entry("marker", &self.marker)?;
191        if !self.title.is_empty() {
192            state.serialize_entry("title", &self.title)?;
193        }
194        if !self.metadata.is_default() {
195            state.serialize_entry("metadata", &self.metadata)?;
196        }
197        state.serialize_entry("items", &self.items)?;
198        state.serialize_entry("location", &self.location)?;
199        state.end()
200    }
201}
202
203impl Serialize for CalloutList<'_> {
204    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
205    where
206        S: Serializer,
207    {
208        let mut state = serializer.serialize_map(None)?;
209        state.serialize_entry("name", "list")?;
210        state.serialize_entry("type", "block")?;
211        state.serialize_entry("variant", "callout")?;
212        if !self.title.is_empty() {
213            state.serialize_entry("title", &self.title)?;
214        }
215        if !self.metadata.is_default() {
216            state.serialize_entry("metadata", &self.metadata)?;
217        }
218        state.serialize_entry("items", &self.items)?;
219        state.serialize_entry("location", &self.location)?;
220        state.end()
221    }
222}
223
224impl Serialize for DescriptionList<'_> {
225    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
226    where
227        S: Serializer,
228    {
229        let mut state = serializer.serialize_map(None)?;
230        state.serialize_entry("name", "dlist")?;
231        state.serialize_entry("type", "block")?;
232        if !self.title.is_empty() {
233            state.serialize_entry("title", &self.title)?;
234        }
235        if !self.metadata.is_default() {
236            state.serialize_entry("metadata", &self.metadata)?;
237        }
238        state.serialize_entry("items", &self.items)?;
239        state.serialize_entry("location", &self.location)?;
240        state.end()
241    }
242}
243
244impl Serialize for ListItem<'_> {
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", "listItem")?;
251        state.serialize_entry("type", "block")?;
252        state.serialize_entry("marker", &self.marker)?;
253        if let Some(checked) = &self.checked {
254            state.serialize_entry("checked", checked)?;
255        }
256        // The TCK doesn't contain level information for list items, so we don't serialize
257        // it.
258        //
259        // Uncomment the line below if level information is added in the future.
260        //
261        // state.serialize_entry("level", &self.level)?;
262        state.serialize_entry("principal", &self.principal)?;
263        if !self.blocks.is_empty() {
264            state.serialize_entry("blocks", &self.blocks)?;
265        }
266        state.serialize_entry("location", &self.location)?;
267        state.end()
268    }
269}
270
271impl Serialize for ListItemCheckedStatus {
272    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
273    where
274        S: Serializer,
275    {
276        match &self {
277            ListItemCheckedStatus::Checked => serializer.serialize_bool(true),
278            ListItemCheckedStatus::Unchecked => serializer.serialize_bool(false),
279        }
280    }
281}
282
283impl Serialize for CalloutListItem<'_> {
284    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
285    where
286        S: Serializer,
287    {
288        let mut state = serializer.serialize_map(None)?;
289        state.serialize_entry("name", "listItem")?;
290        state.serialize_entry("type", "block")?;
291        state.serialize_entry("callout", &self.callout)?;
292        state.serialize_entry("principal", &self.principal)?;
293        if !self.blocks.is_empty() {
294            state.serialize_entry("blocks", &self.blocks)?;
295        }
296        state.serialize_entry("location", &self.location)?;
297        state.end()
298    }
299}