1use super::*;
3
4#[rustfmt::skip] const LIST_INDENTATION: &str = " ";
15const ZERO_PADDING: &str = "00000000000000000000";
16
17#[derive(Debug, PartialEq, Eq)]
19pub enum ListMarker {
20 Ordered {
22 zero_padding: usize,
24 number: usize,
26 marker: OrderedListMarker,
28 },
29 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 #[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 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 zero_padding + (char_len + 2) as usize
86 }
87 Self::Unordered(_) => 2,
88 }
89 }
90}
91
92#[derive(Debug, PartialEq, Eq, Clone)]
94pub enum OrderedListMarker {
95 Period,
97 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#[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#[derive(Debug, PartialEq, Eq, Clone)]
129pub enum UnorderedListMarker {
130 Asterisk,
132 Plus,
134 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#[derive(Debug, PartialEq, Eq)]
163pub enum ParseListMarkerError {
164 NoMarkers,
166 InvalidMarker(InvalidMarker),
168 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}