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::{CalloutRef, 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/// An item in a description list (term + description).
56///
57/// # Structure
58///
59/// ```text
60/// term:: principal text    <- term, delimiter, principal_text
61///        description       <- description (blocks)
62/// ```
63///
64/// # Note on Field Names
65///
66/// - `description` is **singular** (not `descriptions`) - it holds the block content
67///   following the term
68/// - `principal_text` is inline content immediately after the delimiter on the same line
69///
70/// ```
71/// # use acdc_parser::DescriptionListItem;
72/// fn has_description(item: &DescriptionListItem) -> bool {
73///     !item.description.is_empty()  // Note: singular 'description'
74/// }
75/// ```
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77#[non_exhaustive]
78pub struct DescriptionListItem {
79    /// Optional anchors (IDs) attached to this item.
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub anchors: Vec<Anchor>,
82    /// The term being defined (inline content before the delimiter).
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    pub term: Vec<InlineNode>,
85    /// The delimiter used (`::`, `:::`, `::::`, or `;;`).
86    pub delimiter: String,
87    /// Inline content immediately after the delimiter on the same line.
88    #[serde(default, skip_serializing_if = "Vec::is_empty")]
89    pub principal_text: Vec<InlineNode>,
90    /// Block content providing the description (singular, not plural).
91    pub description: Vec<Block>,
92    pub location: Location,
93}
94
95/// A `UnorderedList` represents an unordered list in a document.
96#[derive(Clone, Debug, PartialEq)]
97#[non_exhaustive]
98pub struct UnorderedList {
99    pub title: Title,
100    pub metadata: BlockMetadata,
101    pub items: Vec<ListItem>,
102    pub marker: String,
103    pub location: Location,
104}
105
106/// An `OrderedList` represents an ordered list in a document.
107#[derive(Clone, Debug, PartialEq)]
108#[non_exhaustive]
109pub struct OrderedList {
110    pub title: Title,
111    pub metadata: BlockMetadata,
112    pub items: Vec<ListItem>,
113    pub marker: String,
114    pub location: Location,
115}
116
117/// A `CalloutList` represents a callout list in a document.
118///
119/// Callout lists are used to annotate code blocks with numbered references.
120#[derive(Clone, Debug, PartialEq)]
121#[non_exhaustive]
122pub struct CalloutList {
123    pub title: Title,
124    pub metadata: BlockMetadata,
125    pub items: Vec<CalloutListItem>,
126    pub location: Location,
127}
128
129/// A `CalloutListItem` represents an item in a callout list.
130///
131/// Unlike [`ListItem`], callout list items have a structured [`CalloutRef`] that
132/// preserves whether the original marker was explicit (`<1>`) or auto-numbered (`<.>`).
133///
134/// # Example
135///
136/// ```asciidoc
137/// <1> First explanation
138/// <.> Auto-numbered explanation
139/// ```
140#[derive(Clone, Debug, PartialEq)]
141#[non_exhaustive]
142pub struct CalloutListItem {
143    /// The callout reference (explicit or auto-numbered).
144    pub callout: CalloutRef,
145    /// Principal text - inline content that appears after the callout marker.
146    pub principal: Vec<InlineNode>,
147    /// Attached blocks - blocks attached via continuation (though rarely used for callouts).
148    pub blocks: Vec<Block>,
149    /// Source location of this item.
150    pub location: Location,
151}
152
153// =============================================================================
154// Serialization
155// =============================================================================
156
157macro_rules! impl_list_serialize {
158    ($type:ty, $variant:literal, with_marker) => {
159        impl Serialize for $type {
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", $variant)?;
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    };
181    ($type:ty, $variant:literal) => {
182        impl Serialize for $type {
183            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
184            where
185                S: Serializer,
186            {
187                let mut state = serializer.serialize_map(None)?;
188                state.serialize_entry("name", "list")?;
189                state.serialize_entry("type", "block")?;
190                state.serialize_entry("variant", $variant)?;
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    };
203}
204
205impl_list_serialize!(UnorderedList, "unordered", with_marker);
206impl_list_serialize!(OrderedList, "ordered", with_marker);
207impl_list_serialize!(CalloutList, "callout");
208
209impl Serialize for DescriptionList {
210    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
211    where
212        S: Serializer,
213    {
214        let mut state = serializer.serialize_map(None)?;
215        state.serialize_entry("name", "dlist")?;
216        state.serialize_entry("type", "block")?;
217        if !self.title.is_empty() {
218            state.serialize_entry("title", &self.title)?;
219        }
220        if !self.metadata.is_default() {
221            state.serialize_entry("metadata", &self.metadata)?;
222        }
223        state.serialize_entry("items", &self.items)?;
224        state.serialize_entry("location", &self.location)?;
225        state.end()
226    }
227}
228
229impl Serialize for ListItem {
230    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231    where
232        S: Serializer,
233    {
234        let mut state = serializer.serialize_map(None)?;
235        state.serialize_entry("name", "listItem")?;
236        state.serialize_entry("type", "block")?;
237        state.serialize_entry("marker", &self.marker)?;
238        if let Some(checked) = &self.checked {
239            state.serialize_entry("checked", checked)?;
240        }
241        // The TCK doesn't contain level information for list items, so we don't serialize
242        // it.
243        //
244        // Uncomment the line below if level information is added in the future.
245        //
246        // state.serialize_entry("level", &self.level)?;
247        state.serialize_entry("principal", &self.principal)?;
248        if !self.blocks.is_empty() {
249            state.serialize_entry("blocks", &self.blocks)?;
250        }
251        state.serialize_entry("location", &self.location)?;
252        state.end()
253    }
254}
255
256impl Serialize for ListItemCheckedStatus {
257    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
258    where
259        S: Serializer,
260    {
261        match &self {
262            ListItemCheckedStatus::Checked => serializer.serialize_bool(true),
263            ListItemCheckedStatus::Unchecked => serializer.serialize_bool(false),
264        }
265    }
266}
267
268impl Serialize for CalloutListItem {
269    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
270    where
271        S: Serializer,
272    {
273        let mut state = serializer.serialize_map(None)?;
274        state.serialize_entry("name", "listItem")?;
275        state.serialize_entry("type", "block")?;
276        state.serialize_entry("callout", &self.callout)?;
277        state.serialize_entry("principal", &self.principal)?;
278        if !self.blocks.is_empty() {
279            state.serialize_entry("blocks", &self.blocks)?;
280        }
281        state.serialize_entry("location", &self.location)?;
282        state.end()
283    }
284}
285
286// =============================================================================
287// Deserialization
288// =============================================================================
289
290impl<'de> Deserialize<'de> for ListItem {
291    fn deserialize<D>(deserializer: D) -> Result<ListItem, D::Error>
292    where
293        D: Deserializer<'de>,
294    {
295        struct ListItemVisitor;
296
297        impl<'de> Visitor<'de> for ListItemVisitor {
298            type Value = ListItem;
299
300            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
301                formatter.write_str("a struct representing ListItem")
302            }
303
304            fn visit_map<V>(self, mut map: V) -> Result<ListItem, V::Error>
305            where
306                V: MapAccess<'de>,
307            {
308                let mut my_principal = None;
309                let mut my_blocks = None;
310                let mut my_checked = None;
311                let mut my_location = None;
312                let mut my_marker = None;
313
314                while let Some(key) = map.next_key::<String>()? {
315                    match key.as_str() {
316                        "principal" => {
317                            if my_principal.is_some() {
318                                return Err(de::Error::duplicate_field("principal"));
319                            }
320                            my_principal = Some(map.next_value()?);
321                        }
322                        "blocks" => {
323                            if my_blocks.is_some() {
324                                return Err(de::Error::duplicate_field("blocks"));
325                            }
326                            my_blocks = Some(map.next_value()?);
327                        }
328                        "marker" => {
329                            if my_marker.is_some() {
330                                return Err(de::Error::duplicate_field("marker"));
331                            }
332                            my_marker = Some(map.next_value::<String>()?);
333                        }
334                        "location" => {
335                            if my_location.is_some() {
336                                return Err(de::Error::duplicate_field("location"));
337                            }
338                            my_location = Some(map.next_value()?);
339                        }
340                        "checked" => {
341                            if my_checked.is_some() {
342                                return Err(de::Error::duplicate_field("checked"));
343                            }
344                            my_checked = Some(map.next_value::<bool>()?);
345                        }
346                        _ => {
347                            tracing::debug!(?key, "ignoring unexpected field in ListItem");
348                            // Ignore any other fields
349                            let _ = map.next_value::<de::IgnoredAny>()?;
350                        }
351                    }
352                }
353                let marker = my_marker.ok_or_else(|| de::Error::missing_field("marker"))?;
354                let principal =
355                    my_principal.ok_or_else(|| de::Error::missing_field("principal"))?;
356                let blocks = my_blocks.unwrap_or_default();
357                let level =
358                    ListLevel::try_from(ListItem::parse_depth_from_marker(&marker).unwrap_or(1))
359                        .map_err(|e| {
360                            de::Error::custom(format!("invalid list item level from marker: {e}",))
361                        })?;
362                let checked = my_checked.map(|c| {
363                    if c {
364                        ListItemCheckedStatus::Checked
365                    } else {
366                        ListItemCheckedStatus::Unchecked
367                    }
368                });
369                Ok(ListItem {
370                    level,
371                    marker,
372                    location: my_location.ok_or_else(|| de::Error::missing_field("location"))?,
373                    principal,
374                    blocks,
375                    checked,
376                })
377            }
378        }
379        deserializer.deserialize_map(ListItemVisitor)
380    }
381}
382
383impl<'de> Deserialize<'de> for ListItemCheckedStatus {
384    fn deserialize<D>(deserializer: D) -> Result<ListItemCheckedStatus, D::Error>
385    where
386        D: serde::Deserializer<'de>,
387    {
388        struct ListItemCheckedStatusVisitor;
389
390        impl Visitor<'_> for ListItemCheckedStatusVisitor {
391            type Value = ListItemCheckedStatus;
392
393            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
394                formatter.write_str("a boolean representing checked status")
395            }
396
397            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
398            where
399                E: de::Error,
400            {
401                if v {
402                    Ok(ListItemCheckedStatus::Checked)
403                } else {
404                    Ok(ListItemCheckedStatus::Unchecked)
405                }
406            }
407        }
408
409        deserializer.deserialize_bool(ListItemCheckedStatusVisitor)
410    }
411}
412
413impl<'de> Deserialize<'de> for CalloutListItem {
414    fn deserialize<D>(deserializer: D) -> Result<CalloutListItem, D::Error>
415    where
416        D: Deserializer<'de>,
417    {
418        struct CalloutListItemVisitor;
419
420        impl<'de> Visitor<'de> for CalloutListItemVisitor {
421            type Value = CalloutListItem;
422
423            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
424                formatter.write_str("a struct representing CalloutListItem")
425            }
426
427            fn visit_map<V>(self, mut map: V) -> Result<CalloutListItem, V::Error>
428            where
429                V: MapAccess<'de>,
430            {
431                let mut my_callout = None;
432                let mut my_principal = None;
433                let mut my_blocks = None;
434                let mut my_location = None;
435
436                while let Some(key) = map.next_key::<String>()? {
437                    match key.as_str() {
438                        "callout" => {
439                            if my_callout.is_some() {
440                                return Err(de::Error::duplicate_field("callout"));
441                            }
442                            my_callout = Some(map.next_value()?);
443                        }
444                        "principal" => {
445                            if my_principal.is_some() {
446                                return Err(de::Error::duplicate_field("principal"));
447                            }
448                            my_principal = Some(map.next_value()?);
449                        }
450                        "blocks" => {
451                            if my_blocks.is_some() {
452                                return Err(de::Error::duplicate_field("blocks"));
453                            }
454                            my_blocks = Some(map.next_value()?);
455                        }
456                        "location" => {
457                            if my_location.is_some() {
458                                return Err(de::Error::duplicate_field("location"));
459                            }
460                            my_location = Some(map.next_value()?);
461                        }
462                        _ => {
463                            tracing::debug!(?key, "ignoring unexpected field in CalloutListItem");
464                            let _ = map.next_value::<de::IgnoredAny>()?;
465                        }
466                    }
467                }
468
469                Ok(CalloutListItem {
470                    callout: my_callout.ok_or_else(|| de::Error::missing_field("callout"))?,
471                    principal: my_principal.ok_or_else(|| de::Error::missing_field("principal"))?,
472                    blocks: my_blocks.unwrap_or_default(),
473                    location: my_location.ok_or_else(|| de::Error::missing_field("location"))?,
474                })
475            }
476        }
477        deserializer.deserialize_map(CalloutListItemVisitor)
478    }
479}