acdc_parser/model/
lists.rs

1//! List types for `AsciiDoc` documents.
2
3use serde::{
4    Deserialize, Serialize,
5    de::{self, Deserializer, MapAccess, Visitor},
6    ser::{SerializeMap, Serializer},
7};
8
9use super::Block;
10use super::anchor::Anchor;
11use super::inlines::InlineNode;
12use super::location::Location;
13use super::metadata::BlockMetadata;
14use super::title::Title;
15
16pub type ListLevel = u8;
17
18/// A `ListItemCheckedStatus` represents the checked status of a list item.
19#[derive(Clone, Debug, PartialEq)]
20#[non_exhaustive]
21pub enum ListItemCheckedStatus {
22    Checked,
23    Unchecked,
24}
25
26/// A `ListItem` represents a list item in a document.
27///
28/// List items have principal text (inline content immediately after the marker) and
29/// optionally attached blocks (via continuation or nesting). This matches Asciidoctor's
30/// AST structure where principal text renders as bare `<p>` and attached blocks render
31/// with their full wrapper divs.
32#[derive(Clone, Debug, PartialEq)]
33#[non_exhaustive]
34pub struct ListItem {
35    pub level: ListLevel,
36    pub marker: String,
37    pub checked: Option<ListItemCheckedStatus>,
38    /// Principal text - inline content that appears immediately after the list marker
39    pub principal: Vec<InlineNode>,
40    /// Attached blocks - blocks attached via continuation (+) or nesting
41    pub blocks: Vec<Block>,
42    pub location: Location,
43}
44
45/// A `DescriptionList` represents a description list in a document.
46#[derive(Clone, Debug, PartialEq)]
47#[non_exhaustive]
48pub struct DescriptionList {
49    pub title: Title,
50    pub metadata: BlockMetadata,
51    pub items: Vec<DescriptionListItem>,
52    pub location: Location,
53}
54
55/// A `DescriptionListItem` represents a description list item in a document.
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
57#[non_exhaustive]
58pub struct DescriptionListItem {
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub anchors: Vec<Anchor>,
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    pub term: Vec<InlineNode>,
63    pub delimiter: String,
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub principal_text: Vec<InlineNode>,
66    pub description: Vec<Block>,
67    pub location: Location,
68}
69
70/// A `UnorderedList` represents an unordered list in a document.
71#[derive(Clone, Debug, PartialEq)]
72#[non_exhaustive]
73pub struct UnorderedList {
74    pub title: Title,
75    pub metadata: BlockMetadata,
76    pub items: Vec<ListItem>,
77    pub marker: String,
78    pub location: Location,
79}
80
81/// An `OrderedList` represents an ordered list in a document.
82#[derive(Clone, Debug, PartialEq)]
83#[non_exhaustive]
84pub struct OrderedList {
85    pub title: Title,
86    pub metadata: BlockMetadata,
87    pub items: Vec<ListItem>,
88    pub marker: String,
89    pub location: Location,
90}
91
92/// A `CalloutList` represents a callout list in a document.
93///
94/// Callout lists are used to annotate code blocks with numbered references.
95#[derive(Clone, Debug, PartialEq)]
96#[non_exhaustive]
97pub struct CalloutList {
98    pub title: Title,
99    pub metadata: BlockMetadata,
100    pub items: Vec<ListItem>,
101    pub location: Location,
102}
103
104// =============================================================================
105// Serialization
106// =============================================================================
107
108macro_rules! impl_list_serialize {
109    ($type:ty, $variant:literal, with_marker) => {
110        impl Serialize for $type {
111            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112            where
113                S: Serializer,
114            {
115                let mut state = serializer.serialize_map(None)?;
116                state.serialize_entry("name", "list")?;
117                state.serialize_entry("type", "block")?;
118                state.serialize_entry("variant", $variant)?;
119                state.serialize_entry("marker", &self.marker)?;
120                if !self.title.is_empty() {
121                    state.serialize_entry("title", &self.title)?;
122                }
123                if !self.metadata.is_default() {
124                    state.serialize_entry("metadata", &self.metadata)?;
125                }
126                state.serialize_entry("items", &self.items)?;
127                state.serialize_entry("location", &self.location)?;
128                state.end()
129            }
130        }
131    };
132    ($type:ty, $variant:literal) => {
133        impl Serialize for $type {
134            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135            where
136                S: Serializer,
137            {
138                let mut state = serializer.serialize_map(None)?;
139                state.serialize_entry("name", "list")?;
140                state.serialize_entry("type", "block")?;
141                state.serialize_entry("variant", $variant)?;
142                if !self.title.is_empty() {
143                    state.serialize_entry("title", &self.title)?;
144                }
145                if !self.metadata.is_default() {
146                    state.serialize_entry("metadata", &self.metadata)?;
147                }
148                state.serialize_entry("items", &self.items)?;
149                state.serialize_entry("location", &self.location)?;
150                state.end()
151            }
152        }
153    };
154}
155
156impl_list_serialize!(UnorderedList, "unordered", with_marker);
157impl_list_serialize!(OrderedList, "ordered", with_marker);
158impl_list_serialize!(CalloutList, "callout");
159
160impl Serialize for DescriptionList {
161    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
162    where
163        S: Serializer,
164    {
165        let mut state = serializer.serialize_map(None)?;
166        state.serialize_entry("name", "dlist")?;
167        state.serialize_entry("type", "block")?;
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
180impl Serialize for ListItem {
181    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: Serializer,
184    {
185        let mut state = serializer.serialize_map(None)?;
186        state.serialize_entry("name", "listItem")?;
187        state.serialize_entry("type", "block")?;
188        state.serialize_entry("marker", &self.marker)?;
189        if let Some(checked) = &self.checked {
190            state.serialize_entry("checked", checked)?;
191        }
192        // The TCK doesn't contain level information for list items, so we don't serialize
193        // it.
194        //
195        // Uncomment the line below if level information is added in the future.
196        //
197        // state.serialize_entry("level", &self.level)?;
198        state.serialize_entry("principal", &self.principal)?;
199        if !self.blocks.is_empty() {
200            state.serialize_entry("blocks", &self.blocks)?;
201        }
202        state.serialize_entry("location", &self.location)?;
203        state.end()
204    }
205}
206
207impl Serialize for ListItemCheckedStatus {
208    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
209    where
210        S: Serializer,
211    {
212        match &self {
213            ListItemCheckedStatus::Checked => serializer.serialize_bool(true),
214            ListItemCheckedStatus::Unchecked => serializer.serialize_bool(false),
215        }
216    }
217}
218
219// =============================================================================
220// Deserialization
221// =============================================================================
222
223impl<'de> Deserialize<'de> for ListItem {
224    fn deserialize<D>(deserializer: D) -> Result<ListItem, D::Error>
225    where
226        D: Deserializer<'de>,
227    {
228        struct ListItemVisitor;
229
230        impl<'de> Visitor<'de> for ListItemVisitor {
231            type Value = ListItem;
232
233            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
234                formatter.write_str("a struct representing ListItem")
235            }
236
237            fn visit_map<V>(self, mut map: V) -> Result<ListItem, V::Error>
238            where
239                V: MapAccess<'de>,
240            {
241                let mut my_principal = None;
242                let mut my_blocks = None;
243                let mut my_checked = None;
244                let mut my_location = None;
245                let mut my_marker = None;
246
247                while let Some(key) = map.next_key::<String>()? {
248                    match key.as_str() {
249                        "principal" => {
250                            if my_principal.is_some() {
251                                return Err(de::Error::duplicate_field("principal"));
252                            }
253                            my_principal = Some(map.next_value()?);
254                        }
255                        "blocks" => {
256                            if my_blocks.is_some() {
257                                return Err(de::Error::duplicate_field("blocks"));
258                            }
259                            my_blocks = Some(map.next_value()?);
260                        }
261                        "marker" => {
262                            if my_marker.is_some() {
263                                return Err(de::Error::duplicate_field("marker"));
264                            }
265                            my_marker = Some(map.next_value::<String>()?);
266                        }
267                        "location" => {
268                            if my_location.is_some() {
269                                return Err(de::Error::duplicate_field("location"));
270                            }
271                            my_location = Some(map.next_value()?);
272                        }
273                        "checked" => {
274                            if my_checked.is_some() {
275                                return Err(de::Error::duplicate_field("checked"));
276                            }
277                            my_checked = Some(map.next_value::<bool>()?);
278                        }
279                        _ => {
280                            tracing::debug!(?key, "ignoring unexpected field in ListItem");
281                            // Ignore any other fields
282                            let _ = map.next_value::<de::IgnoredAny>()?;
283                        }
284                    }
285                }
286                let marker = my_marker.ok_or_else(|| de::Error::missing_field("marker"))?;
287                let principal =
288                    my_principal.ok_or_else(|| de::Error::missing_field("principal"))?;
289                let blocks = my_blocks.unwrap_or_default();
290                let level =
291                    ListLevel::try_from(ListItem::parse_depth_from_marker(&marker).unwrap_or(1))
292                        .map_err(|e| {
293                            de::Error::custom(format!("invalid list item level from marker: {e}",))
294                        })?;
295                let checked = my_checked.map(|c| {
296                    if c {
297                        ListItemCheckedStatus::Checked
298                    } else {
299                        ListItemCheckedStatus::Unchecked
300                    }
301                });
302                Ok(ListItem {
303                    level,
304                    marker,
305                    location: my_location.ok_or_else(|| de::Error::missing_field("location"))?,
306                    principal,
307                    blocks,
308                    checked,
309                })
310            }
311        }
312        deserializer.deserialize_map(ListItemVisitor)
313    }
314}
315
316impl<'de> Deserialize<'de> for ListItemCheckedStatus {
317    fn deserialize<D>(deserializer: D) -> Result<ListItemCheckedStatus, D::Error>
318    where
319        D: serde::Deserializer<'de>,
320    {
321        struct ListItemCheckedStatusVisitor;
322
323        impl Visitor<'_> for ListItemCheckedStatusVisitor {
324            type Value = ListItemCheckedStatus;
325
326            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
327                formatter.write_str("a boolean representing checked status")
328            }
329
330            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
331            where
332                E: de::Error,
333            {
334                if v {
335                    Ok(ListItemCheckedStatus::Checked)
336                } else {
337                    Ok(ListItemCheckedStatus::Unchecked)
338                }
339            }
340        }
341
342        deserializer.deserialize_bool(ListItemCheckedStatusVisitor)
343    }
344}