markdown_fmt/
list.rs

1//! List marker types.
2use super::*;
3
4// Including all these spaces might be overkill, but it probably doesn't hurt.
5// In practice we'll see far fewer digits in an ordered list.
6//
7// <https://github.github.com/gfm/#list-items> mentions that:
8//
9//     An ordered list marker is a sequence of 1–9 arabic digits (0-9), followed by either a .
10//     character or a ) character. (The reason for the length limit is that with 10 digits we
11//     start seeing integer overflows in some browsers.)
12//
13#[rustfmt::skip] // RustFmt chocks on this.
14const LIST_INDENTATION: &str = "                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                ";
15const ZERO_PADDING: &str = "00000000000000000000";
16
17/// Marker for the beginning of a list, e.g., `1.` or `*`.
18#[derive(Debug, PartialEq, Eq)]
19pub enum ListMarker {
20    /// An ordered list marker, e.g., `8.` or `013)`.
21    Ordered {
22        /// Number of `0` padding before the number.
23        zero_padding: usize,
24        /// The number part of the marker.
25        number: usize,
26        /// The marker symble.
27        marker: OrderedListMarker,
28    },
29    /// An unordered list marker, e.g., `+` or `-`.
30    Unordered(UnorderedListMarker),
31}
32
33impl std::default::Default for ListMarker {
34    fn default() -> Self {
35        ListMarker::Unordered(UnorderedListMarker::Asterisk)
36    }
37}
38
39impl ListMarker {
40    // TODO(ytmimi) Add a configuration to allow incrementing ordered lists
41    #[allow(dead_code)]
42    pub(super) fn increment_count(&mut self) {
43        match self {
44            Self::Ordered { number, .. } => {
45                *number += 1;
46            }
47            Self::Unordered(_) => {}
48        }
49    }
50
51    pub(super) fn indentation(&self) -> Cow<'static, str> {
52        let indent_index = self.indentation_len();
53
54        if indent_index <= LIST_INDENTATION.len() {
55            Cow::from(&LIST_INDENTATION[..indent_index])
56        } else {
57            // I think it would be extreamly rare to hit his case
58            Cow::from(" ".repeat(indent_index))
59        }
60    }
61
62    pub(super) fn marker_char(&self) -> char {
63        match self {
64            Self::Ordered { marker, .. } => marker.into(),
65            Self::Unordered(marker) => marker.into(),
66        }
67    }
68
69    pub(super) fn zero_padding(&self) -> &'static str {
70        match self {
71            Self::Ordered { zero_padding, .. } => &ZERO_PADDING[..*zero_padding],
72            Self::Unordered(_) => "",
73        }
74    }
75
76    fn indentation_len(&self) -> usize {
77        match self {
78            Self::Ordered {
79                zero_padding,
80                number,
81                ..
82            } => {
83                let char_len = number.checked_ilog10().unwrap_or(0) + 1;
84                // + 2 to for '. '
85                zero_padding + (char_len + 2) as usize
86            }
87            Self::Unordered(_) => 2,
88        }
89    }
90}
91
92/// Marker symbol after the number for ordered lists.
93#[derive(Debug, PartialEq, Eq, Clone)]
94pub enum OrderedListMarker {
95    /// `.`.
96    Period,
97    /// `)`.
98    Parenthesis,
99}
100
101impl From<&OrderedListMarker> for char {
102    fn from(value: &OrderedListMarker) -> Self {
103        match value {
104            OrderedListMarker::Period => '.',
105            OrderedListMarker::Parenthesis => ')',
106        }
107    }
108}
109
110/// Invalid character encountered when parsing a list marker.
111#[repr(transparent)]
112#[derive(Debug, PartialEq, Eq)]
113pub struct InvalidMarker(char);
114
115impl TryFrom<char> for OrderedListMarker {
116    type Error = InvalidMarker;
117
118    fn try_from(value: char) -> Result<Self, Self::Error> {
119        match value {
120            '.' => Ok(OrderedListMarker::Period),
121            ')' => Ok(OrderedListMarker::Parenthesis),
122            _ => Err(InvalidMarker(value)),
123        }
124    }
125}
126
127/// Marker symbol for unordered lists.
128#[derive(Debug, PartialEq, Eq, Clone)]
129pub enum UnorderedListMarker {
130    /// `*`.
131    Asterisk,
132    /// `+`.
133    Plus,
134    /// `-`.
135    Hyphen,
136}
137
138impl From<&UnorderedListMarker> for char {
139    fn from(value: &UnorderedListMarker) -> Self {
140        match value {
141            UnorderedListMarker::Asterisk => '*',
142            UnorderedListMarker::Plus => '+',
143            UnorderedListMarker::Hyphen => '-',
144        }
145    }
146}
147
148impl TryFrom<char> for UnorderedListMarker {
149    type Error = InvalidMarker;
150
151    fn try_from(value: char) -> Result<Self, Self::Error> {
152        match value {
153            '*' => Ok(UnorderedListMarker::Asterisk),
154            '+' => Ok(UnorderedListMarker::Plus),
155            '-' => Ok(UnorderedListMarker::Hyphen),
156            _ => Err(InvalidMarker(value)),
157        }
158    }
159}
160
161/// Some error occured when parsing a ListMarker from a &str
162#[derive(Debug, PartialEq, Eq)]
163pub enum ParseListMarkerError {
164    /// Did not contain the correct list markers.
165    NoMarkers,
166    /// Invalid char where a list marker was expected
167    InvalidMarker(InvalidMarker),
168    /// Failed to parse an integer for ordered lists
169    ParseIntError(ParseIntError),
170}
171
172impl From<InvalidMarker> for ParseListMarkerError {
173    fn from(value: InvalidMarker) -> Self {
174        Self::InvalidMarker(value)
175    }
176}
177
178impl From<ParseIntError> for ParseListMarkerError {
179    fn from(value: ParseIntError) -> Self {
180        Self::ParseIntError(value)
181    }
182}
183
184impl std::str::FromStr for ListMarker {
185    type Err = ParseListMarkerError;
186
187    fn from_str(s: &str) -> Result<Self, Self::Err> {
188        let s = s.trim();
189        if s.is_empty() {
190            return Err(ParseListMarkerError::NoMarkers);
191        }
192
193        if let Some(c @ ('*' | '+' | '-')) = s.chars().next() {
194            return Ok(ListMarker::Unordered(c.try_into()?));
195        }
196
197        let Some((offset, marker)) = s.char_indices().find(|(_, c)| matches!(c, '.' | ')')) else {
198            return Err(ParseListMarkerError::NoMarkers);
199        };
200
201        let number: usize = s[..offset].parse()?;
202        let zero_padding = if number != 0 {
203            s[..offset].bytes().take_while(|b| *b == b'0').count()
204        } else {
205            0
206        };
207
208        Ok(ListMarker::Ordered {
209            zero_padding,
210            number,
211            marker: marker.try_into()?,
212        })
213    }
214}
215
216#[cfg(test)]
217mod test {
218    use super::*;
219
220    macro_rules! check_unordered_list {
221        ($string:literal, marker=$m:ident) => {
222            assert_eq!(
223                ListMarker::from_str($string),
224                Ok(ListMarker::Unordered(UnorderedListMarker::$m))
225            );
226        };
227    }
228
229    #[test]
230    fn parse_unordered_lists() {
231        check_unordered_list!(" *", marker = Asterisk);
232        check_unordered_list!(" +", marker = Plus);
233        check_unordered_list!(" -", marker = Hyphen);
234        check_unordered_list!("*", marker = Asterisk);
235        check_unordered_list!("+", marker = Plus);
236        check_unordered_list!("-", marker = Hyphen);
237        check_unordered_list!("* foo", marker = Asterisk);
238        check_unordered_list!("+ foo", marker = Plus);
239        check_unordered_list!("- foo", marker = Hyphen);
240        check_unordered_list!("* # Bar", marker = Asterisk);
241        check_unordered_list!("+ # Bar", marker = Plus);
242        check_unordered_list!("- # Bar", marker = Hyphen);
243    }
244
245    macro_rules! check_ordered_list {
246        ($string:literal, number=$n:literal, padding=$p:literal, marker=$m:ident) => {
247            assert_eq!(
248                ListMarker::from_str($string),
249                Ok(ListMarker::Ordered {
250                    zero_padding: $p,
251                    number: $n,
252                    marker: OrderedListMarker::$m
253                })
254            );
255        };
256    }
257
258    #[test]
259    fn parse_ordered_lists() {
260        check_ordered_list!("1.", number = 1, padding = 0, marker = Period);
261        check_ordered_list!("1)", number = 1, padding = 0, marker = Parenthesis);
262        check_ordered_list!("20.", number = 20, padding = 0, marker = Period);
263        check_ordered_list!("20)", number = 20, padding = 0, marker = Parenthesis);
264        check_ordered_list!("003.", number = 3, padding = 2, marker = Period);
265        check_ordered_list!("003)", number = 3, padding = 2, marker = Parenthesis);
266    }
267}