1use std::sync::LazyLock;
2
3use regex::Regex;
4
5use crate::{
6 HasSpan, Parser, Span,
7 content::{Content, SubstitutionStep},
8 document::RefType,
9 span::MatchedItem,
10 warnings::{Warning, WarningType},
11};
12
13#[derive(Clone, Eq, PartialEq)]
15pub enum ListItemMarker<'src> {
16 Hyphen(Span<'src>),
18
19 Asterisks(Span<'src>),
21
22 Bullet(Span<'src>),
24
25 Dots(Span<'src>),
27
28 AlphaListCapital(Span<'src>),
30
31 AlphaListLower(Span<'src>),
33
34 RomanNumeralLower(Span<'src>),
36
37 RomanNumeralUpper(Span<'src>),
39
40 ArabicNumeral(Span<'src>),
42
43 Callout(Span<'src>),
46
47 DefinedTerm {
49 term: Content<'src>,
51
52 marker: Span<'src>,
54
55 source: Span<'src>,
57 },
58}
59
60impl<'src> ListItemMarker<'src> {
61 pub(crate) fn starts_with_marker(source: Span<'src>) -> bool {
62 let source = source.discard_whitespace();
66 LIST_ITEM_MARKER.is_match(source.data()) || CALLOUT_LIST_MARKER.is_match(source.data())
67 }
68
69 pub(crate) fn parse(source: Span<'src>, parser: &Parser) -> Option<MatchedItem<'src, Self>> {
70 let source = source.discard_whitespace();
71
72 if let Some(captures) = CALLOUT_LIST_MARKER.captures(source.data()) {
75 let marker = source.slice(0..captures[1].len());
76 let after = source.slice_from(captures[1].len()..).discard_whitespace();
77
78 return Some(MatchedItem {
79 item: Self::Callout(marker),
80 after,
81 });
82 }
83
84 if let Some(captures) = LIST_ITEM_MARKER.captures(source.data()) {
85 let marker = source.slice(0..captures[1].len());
86 let marker_str = marker.data();
87 let after = source.slice_from(captures[1].len()..).discard_whitespace();
88
89 let first_char = captures[1].chars().next();
90
91 let item = if marker_str == "-" {
92 Self::Hyphen(marker)
93 } else if marker_str.starts_with('*') {
94 Self::Asterisks(marker)
95 } else if marker_str == "•" {
96 Self::Bullet(marker)
97 } else if marker_str.starts_with('.') {
98 Self::Dots(marker)
99 } else if let Some(first_char) = first_char
100 && first_char.is_ascii_uppercase()
101 && marker_str.ends_with('.')
102 {
103 Self::AlphaListCapital(marker)
104 } else if let Some(first_char) = first_char
105 && first_char.is_ascii_lowercase()
106 && marker_str.ends_with('.')
107 {
108 Self::AlphaListLower(marker)
109 } else if marker_str.ends_with(')')
110 && marker_str
111 .chars()
112 .take(marker_str.len() - 1)
113 .all(|c| "ivxlcdm".contains(c))
114 {
115 Self::RomanNumeralLower(marker)
116 } else if marker_str.ends_with(')')
117 && marker_str
118 .chars()
119 .take(marker_str.len() - 1)
120 .all(|c| "IVXLCDM".contains(c))
121 {
122 Self::RomanNumeralUpper(marker)
123 } else if marker_str.ends_with('.')
124 && marker_str
125 .chars()
126 .take(marker_str.len() - 1)
127 .all(|c| c.is_ascii_digit())
128 {
129 Self::ArabicNumeral(marker)
130 } else {
131 return None;
133 };
134
135 Some(MatchedItem { item, after })
136 } else {
137 let source_data = source.data();
140 if source_data.starts_with("//") && !source_data.starts_with("///") {
141 return None;
142 }
143
144 let captures = DESCRIPTION_LIST_MARKER.captures(source_data)?;
145
146 let full_match = captures.get(0)?;
149 if full_match.start() != 0 {
150 return None;
151 }
152
153 let after = source.slice_from(full_match.end()..).discard_whitespace();
154
155 let source = source
156 .slice_to(..full_match.end())
157 .trim_trailing_whitespace();
158
159 let term_len = captures[1].len();
160 let term = source.slice(0..term_len);
161 let mut term: Content<'src> = term.into();
162
163 SubstitutionStep::AttributeReferences.apply(&mut term, parser, None);
167
168 let marker = source.slice_from(term_len..);
169
170 Some(MatchedItem {
171 item: Self::DefinedTerm {
172 term,
173 marker,
174 source,
175 },
176 after,
177 })
178 }
179 }
180
181 pub(crate) fn register_leading_anchors(
191 &mut self,
192 parser: &mut Parser,
193 warnings: &mut Vec<Warning<'src>>,
194 ) {
195 let Self::DefinedTerm {
196 term,
197 marker: _,
198 source: _,
199 } = self
200 else {
201 return;
202 };
203
204 let term_text = term.rendered();
206 if !term_text.starts_with("[[") {
207 SubstitutionStep::Macros.apply(term, parser, None);
209 return;
210 }
211
212 if let Some(captures) = LEADING_INLINE_ANCHOR.captures(term_text) {
219 let id = &captures[1];
220
221 let reftext = captures.get(2).map(|m| m.as_str().to_string()).or_else(|| {
224 captures.get(3).map(|m| m.as_str().trim().to_string())
226 });
227
228 if let Err(_duplicate_error) =
230 parser.register_ref(id, reftext.as_deref(), RefType::Anchor)
231 {
232 warnings.push(Warning {
233 source: term.original(),
234 warning: WarningType::DuplicateId(id.to_string()),
235 });
236 }
237 }
238
239 SubstitutionStep::Macros.apply(term, parser, None);
241 }
242
243 pub(crate) fn term_mut(&mut self) -> Option<&mut Content<'src>> {
246 match self {
247 Self::DefinedTerm { term, .. } => Some(term),
248 _ => None,
249 }
250 }
251
252 pub(crate) fn callout_number(&self) -> Option<u32> {
255 match self {
256 Self::Callout(span) => span
257 .data()
258 .trim_start_matches('<')
259 .trim_end_matches('>')
260 .parse::<u32>()
261 .ok(),
262 _ => None,
263 }
264 }
265
266 pub(crate) fn is_match_for(&self, other: &Self) -> bool {
268 match self {
269 Self::Hyphen(self_span) => match other {
270 Self::Hyphen(other_span) => self_span.data() == other_span.data(),
271 _ => false,
272 },
273
274 Self::Asterisks(self_span) => match other {
275 Self::Asterisks(other_span) => self_span.data() == other_span.data(),
276 _ => false,
277 },
278
279 Self::Bullet(self_span) => match other {
280 Self::Bullet(other_span) => self_span.data() == other_span.data(),
281 _ => false,
282 },
283
284 Self::Dots(self_span) => match other {
285 Self::Dots(other_span) => self_span.data() == other_span.data(),
286 _ => false,
287 },
288
289 Self::AlphaListCapital(_self_span) => {
290 matches!(other, Self::AlphaListCapital(_other_span))
291 }
292
293 Self::AlphaListLower(_self_span) => {
294 matches!(other, Self::AlphaListLower(_other_span))
295 }
296
297 Self::RomanNumeralLower(_self_span) => {
298 matches!(other, Self::RomanNumeralLower(_other_span))
299 }
300
301 Self::RomanNumeralUpper(_self_span) => {
302 matches!(other, Self::RomanNumeralUpper(_other_span))
303 }
304
305 Self::ArabicNumeral(_self_span) => {
306 matches!(other, Self::ArabicNumeral(_other_span))
307 }
308
309 Self::Callout(_self_span) => {
310 matches!(other, Self::Callout(_other_span))
311 }
312
313 Self::DefinedTerm {
314 term: _,
315 marker: self_marker,
316 source: _,
317 } => match other {
318 Self::DefinedTerm {
319 term: _,
320 marker: other_marker,
321 source: _,
322 } => self_marker.data() == other_marker.data(),
323 _ => false,
324 },
325 }
326 }
327
328 pub(crate) fn ordinal_value(&self) -> Option<u32> {
334 match self {
335 Self::AlphaListLower(span) => {
336 let ch = span.data().chars().next()?;
338 Some((ch as u32) - ('a' as u32) + 1)
339 }
340
341 Self::AlphaListCapital(span) => {
342 let ch = span.data().chars().next()?;
344 Some((ch as u32) - ('A' as u32) + 1)
345 }
346
347 Self::ArabicNumeral(span) => {
348 span.data().trim_end_matches('.').parse().ok()
350 }
351
352 Self::RomanNumeralLower(span) => {
353 parse_roman_numeral(span.data().trim_end_matches(')'))
355 }
356
357 Self::RomanNumeralUpper(span) => {
358 parse_roman_numeral(span.data().trim_end_matches(')'))
360 }
361
362 _ => None,
364 }
365 }
366
367 pub(crate) fn ordinal_to_marker_text(&self, ordinal: u32) -> Option<String> {
372 match self {
373 Self::AlphaListLower(_) => {
374 char::from_u32('a' as u32 + ordinal - 1).map(|c| c.to_string())
376 }
377
378 Self::AlphaListCapital(_) => {
379 char::from_u32('A' as u32 + ordinal - 1).map(|c| c.to_string())
381 }
382
383 Self::ArabicNumeral(_) => {
384 Some(ordinal.to_string())
386 }
387
388 Self::RomanNumeralLower(_) => {
389 Some(to_roman_numeral_lower(ordinal))
391 }
392
393 Self::RomanNumeralUpper(_) => {
394 Some(to_roman_numeral_upper(ordinal))
396 }
397
398 _ => None,
400 }
401 }
402}
403
404impl<'src> HasSpan<'src> for ListItemMarker<'src> {
405 fn span(&self) -> Span<'src> {
406 match self {
407 Self::Hyphen(x) => *x,
408 Self::Asterisks(x) => *x,
409 Self::Bullet(x) => *x,
410 Self::Dots(x) => *x,
411 Self::AlphaListCapital(x) => *x,
412 Self::AlphaListLower(x) => *x,
413 Self::RomanNumeralLower(x) => *x,
414 Self::RomanNumeralUpper(x) => *x,
415 Self::ArabicNumeral(x) => *x,
416 Self::Callout(x) => *x,
417
418 Self::DefinedTerm {
419 term: _,
420 marker: _,
421 source,
422 } => *source,
423 }
424 }
425}
426
427impl std::fmt::Debug for ListItemMarker<'_> {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 match self {
430 Self::Hyphen(x) => f.debug_tuple("ListItemMarker::Hyphen").field(x).finish(),
431 Self::Asterisks(x) => f.debug_tuple("ListItemMarker::Asterisks").field(x).finish(),
432 Self::Bullet(x) => f.debug_tuple("ListItemMarker::Bullet").field(x).finish(),
433 Self::Dots(x) => f.debug_tuple("ListItemMarker::Dots").field(x).finish(),
434
435 Self::AlphaListCapital(x) => f
436 .debug_tuple("ListItemMarker::AlphaListCapital")
437 .field(x)
438 .finish(),
439
440 Self::AlphaListLower(x) => f
441 .debug_tuple("ListItemMarker::AlphaListLower")
442 .field(x)
443 .finish(),
444
445 Self::RomanNumeralLower(x) => f
446 .debug_tuple("ListItemMarker::RomanNumeralLower")
447 .field(x)
448 .finish(),
449
450 Self::RomanNumeralUpper(x) => f
451 .debug_tuple("ListItemMarker::RomanNumeralUpper")
452 .field(x)
453 .finish(),
454
455 Self::ArabicNumeral(x) => f
456 .debug_tuple("ListItemMarker::ArabicNumeral")
457 .field(x)
458 .finish(),
459
460 Self::Callout(x) => f.debug_tuple("ListItemMarker::Callout").field(x).finish(),
461
462 Self::DefinedTerm {
463 term,
464 marker,
465 source,
466 } => f
467 .debug_struct("ListItemMarker::DefinedTerm")
468 .field("term", term)
469 .field("marker", marker)
470 .field("source", source)
471 .finish(),
472 }
473 }
474}
475
476static LIST_ITEM_MARKER: LazyLock<Regex> = LazyLock::new(|| {
477 #[allow(clippy::unwrap_used)]
478 Regex::new(
479 r#"(?x)
480 ^( # Capture group for list marker
481 - # Hyphen (unordered list)
482 |\*+ # One or more asterisks (unordered list, up to 5 levels)
483 |\.+ # One or more dots (ordered list, up to 5 levels)
484 |\u{2022} # Bullet character • (unordered list)
485 |\d+\. # Digits followed by dot (numbered list)
486 |[a-zA-Z]\. # Letter followed by dot (alpha list)
487 |[ivxlcdm]+\) # Lowercase Roman numerals followed by )
488 |[IVXLCDM]+\) # Uppercase Roman numerals followed by )
489 )
490 [\ \t] # Required whitespace after marker
491 "#,
492 )
493 .unwrap()
494});
495
496static CALLOUT_LIST_MARKER: LazyLock<Regex> = LazyLock::new(|| {
499 #[allow(clippy::unwrap_used)]
500 Regex::new(
501 r#"(?x)
502 ^ # Start of line
503 ( # Capture group 1: the marker
504 <(?:\d+|\.)> # `<` then digits or a dot, then `>`
505 )
506 [\ \t] # Required whitespace after marker
507 "#,
508 )
509 .unwrap()
510});
511
512static DESCRIPTION_LIST_MARKER: LazyLock<Regex> = LazyLock::new(|| {
513 #[allow(clippy::unwrap_used)]
514 Regex::new(
515 r#"(?xm)
516 ^ # Start of line
517 ( # Capture group 1: Term being defined
518 [^\ \t] # At least one non-whitespace character (start of term)
519 .*? # Any characters (rest of term, non-greedy)
520 )
521 (?::::?:?|;;) # Delimiter: ::, :::, ::::, or ;;
522 (?:$|[\ \t]) # End of line or whitespace after marker
523 "#,
524 )
525 .unwrap()
526});
527
528static LEADING_INLINE_ANCHOR: LazyLock<Regex> = LazyLock::new(|| {
536 #[allow(clippy::unwrap_used)]
537 Regex::new(
538 r#"(?x)
539 ^ # Start of string
540 \[\[ # Opening [[
541 ([\p{Alphabetic}_:] # (1) Anchor ID: first char must be letter, _, or :
542 [\p{Alphabetic}\p{Nd}_\-:.]*) # Rest of ID: letters, digits, _, -, :, .
543 (?:,\s*([^\]]+))? # (2) Optional reftext after comma
544 \]\] # Closing ]]
545 (.*) # (3) Text after the anchor
546 "#,
547 )
548 .unwrap()
549});
550
551fn parse_roman_numeral(s: &str) -> Option<u32> {
553 let mut result: u32 = 0;
554 let mut prev_value: u32 = 0;
555
556 for ch in s.chars().rev() {
557 let value = match ch {
558 'i' | 'I' => 1,
559 'v' | 'V' => 5,
560 'x' | 'X' => 10,
561 'l' | 'L' => 50,
562 'c' | 'C' => 100,
563 'd' | 'D' => 500,
564 'm' | 'M' => 1000,
565 _ => return None,
566 };
567
568 if value < prev_value {
569 result -= value;
570 } else {
571 result += value;
572 }
573 prev_value = value;
574 }
575
576 if result > 0 { Some(result) } else { None }
577}
578
579fn to_roman_numeral_lower(mut n: u32) -> String {
581 const NUMERALS: &[(u32, &str)] = &[
582 (1000, "m"),
583 (900, "cm"),
584 (500, "d"),
585 (400, "cd"),
586 (100, "c"),
587 (90, "xc"),
588 (50, "l"),
589 (40, "xl"),
590 (10, "x"),
591 (9, "ix"),
592 (5, "v"),
593 (4, "iv"),
594 (1, "i"),
595 ];
596
597 let mut result = String::new();
598 for &(value, numeral) in NUMERALS {
599 while n >= value {
600 result.push_str(numeral);
601 n -= value;
602 }
603 }
604 result
605}
606
607fn to_roman_numeral_upper(mut n: u32) -> String {
609 const NUMERALS: &[(u32, &str)] = &[
610 (1000, "M"),
611 (900, "CM"),
612 (500, "D"),
613 (400, "CD"),
614 (100, "C"),
615 (90, "XC"),
616 (50, "L"),
617 (40, "XL"),
618 (10, "X"),
619 (9, "IX"),
620 (5, "V"),
621 (4, "IV"),
622 (1, "I"),
623 ];
624
625 let mut result = String::new();
626 for &(value, numeral) in NUMERALS {
627 while n >= value {
628 result.push_str(numeral);
629 n -= value;
630 }
631 }
632 result
633}
634
635#[cfg(test)]
636mod tests {
637 #![allow(clippy::panic)]
638 #![allow(clippy::unwrap_used)]
639
640 use crate::{span::MatchedItem, tests::prelude::*};
641
642 fn lim_parse<'a>(
643 source: &'a str,
644 ) -> Option<MatchedItem<'a, crate::blocks::ListItemMarker<'a>>> {
645 let parser = Parser::default();
646 crate::blocks::ListItemMarker::parse(crate::Span::new(source), &parser)
647 }
648
649 #[test]
650 fn hyphen() {
651 assert!(lim_parse("-").is_none());
652 assert!(lim_parse("-- x").is_none());
653
654 let lim = lim_parse("- blah").unwrap();
655
656 assert_eq!(
657 lim.item,
658 ListItemMarker::Hyphen(Span {
659 data: "-",
660 line: 1,
661 col: 1,
662 offset: 0,
663 },)
664 );
665
666 assert_eq!(
667 lim.after,
668 Span {
669 data: "blah",
670 line: 1,
671 col: 3,
672 offset: 2,
673 }
674 );
675
676 assert_eq!(
677 lim.item.span(),
678 Span {
679 data: "-",
680 line: 1,
681 col: 1,
682 offset: 0,
683 }
684 );
685
686 assert_eq!(
687 format!("{lim:#?}", lim = lim.item),
688 "ListItemMarker::Hyphen(\n Span {\n data: \"-\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
689 );
690 }
691
692 #[test]
693 fn asterisks() {
694 assert!(lim_parse("*").is_none());
695 assert!(lim_parse("*- x").is_none());
696
697 let lim = lim_parse("* blah").unwrap();
698
699 assert_eq!(
700 lim.item,
701 ListItemMarker::Asterisks(Span {
702 data: "*",
703 line: 1,
704 col: 1,
705 offset: 0,
706 },)
707 );
708
709 assert_eq!(
710 lim.after,
711 Span {
712 data: "blah",
713 line: 1,
714 col: 3,
715 offset: 2,
716 }
717 );
718
719 assert_eq!(
720 lim.item.span(),
721 Span {
722 data: "*",
723 line: 1,
724 col: 1,
725 offset: 0,
726 }
727 );
728
729 assert_eq!(
730 format!("{lim:#?}", lim = lim.item),
731 "ListItemMarker::Asterisks(\n Span {\n data: \"*\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
732 );
733
734 let lim = lim_parse("***** blah").unwrap();
735
736 assert_eq!(
737 lim.item,
738 ListItemMarker::Asterisks(Span {
739 data: "*****",
740 line: 1,
741 col: 1,
742 offset: 0,
743 },)
744 );
745
746 assert_eq!(
747 lim.after,
748 Span {
749 data: "blah",
750 line: 1,
751 col: 7,
752 offset: 6,
753 }
754 );
755
756 assert_eq!(
757 lim.item.span(),
758 Span {
759 data: "*****",
760 line: 1,
761 col: 1,
762 offset: 0,
763 }
764 );
765
766 assert_eq!(
767 format!("{lim:#?}", lim = lim.item),
768 "ListItemMarker::Asterisks(\n Span {\n data: \"*****\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
769 );
770 }
771
772 #[test]
773 fn dots() {
774 assert!(lim_parse(".").is_none());
775 assert!(lim_parse(".- x").is_none());
776
777 let lim = lim_parse(". blah").unwrap();
778
779 assert_eq!(
780 lim.item,
781 ListItemMarker::Dots(Span {
782 data: ".",
783 line: 1,
784 col: 1,
785 offset: 0,
786 },)
787 );
788
789 assert_eq!(
790 lim.after,
791 Span {
792 data: "blah",
793 line: 1,
794 col: 3,
795 offset: 2,
796 }
797 );
798
799 assert_eq!(
800 lim.item.span(),
801 Span {
802 data: ".",
803 line: 1,
804 col: 1,
805 offset: 0,
806 }
807 );
808
809 assert_eq!(
810 format!("{lim:#?}", lim = lim.item),
811 "ListItemMarker::Dots(\n Span {\n data: \".\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
812 );
813
814 let lim = lim_parse("..... blah").unwrap();
815
816 assert_eq!(
817 lim.item,
818 ListItemMarker::Dots(Span {
819 data: ".....",
820 line: 1,
821 col: 1,
822 offset: 0,
823 },)
824 );
825
826 assert_eq!(
827 lim.after,
828 Span {
829 data: "blah",
830 line: 1,
831 col: 7,
832 offset: 6,
833 }
834 );
835
836 assert_eq!(
837 lim.item.span(),
838 Span {
839 data: ".....",
840 line: 1,
841 col: 1,
842 offset: 0,
843 }
844 );
845
846 assert_eq!(
847 format!("{lim:#?}", lim = lim.item),
848 "ListItemMarker::Dots(\n Span {\n data: \".....\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
849 );
850 }
851
852 #[test]
853 fn roman_numeral_lower() {
854 assert!(lim_parse("i").is_none());
855 assert!(lim_parse("i.").is_none());
856
857 let lim = lim_parse("i) blah").unwrap();
858
859 assert_eq!(
860 lim.item,
861 ListItemMarker::RomanNumeralLower(Span {
862 data: "i)",
863 line: 1,
864 col: 1,
865 offset: 0,
866 },)
867 );
868
869 assert_eq!(
870 lim.after,
871 Span {
872 data: "blah",
873 line: 1,
874 col: 4,
875 offset: 3,
876 }
877 );
878
879 assert_eq!(
880 lim.item.span(),
881 Span {
882 data: "i)",
883 line: 1,
884 col: 1,
885 offset: 0,
886 }
887 );
888
889 assert_eq!(
890 format!("{lim:#?}", lim = lim.item),
891 "ListItemMarker::RomanNumeralLower(\n Span {\n data: \"i)\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
892 );
893
894 let lim = lim_parse("xvii) blah").unwrap();
895
896 assert_eq!(
897 lim.item,
898 ListItemMarker::RomanNumeralLower(Span {
899 data: "xvii)",
900 line: 1,
901 col: 1,
902 offset: 0,
903 },)
904 );
905
906 assert_eq!(
907 lim.after,
908 Span {
909 data: "blah",
910 line: 1,
911 col: 7,
912 offset: 6,
913 }
914 );
915 }
916
917 #[test]
918 fn roman_numeral_upper() {
919 assert!(lim_parse("I").is_none());
920 assert!(lim_parse("I.").is_none());
921
922 let lim = lim_parse("I) blah").unwrap();
923
924 assert_eq!(
925 lim.item,
926 ListItemMarker::RomanNumeralUpper(Span {
927 data: "I)",
928 line: 1,
929 col: 1,
930 offset: 0,
931 },)
932 );
933
934 assert_eq!(
935 lim.after,
936 Span {
937 data: "blah",
938 line: 1,
939 col: 4,
940 offset: 3,
941 }
942 );
943
944 assert_eq!(
945 lim.item.span(),
946 Span {
947 data: "I)",
948 line: 1,
949 col: 1,
950 offset: 0,
951 }
952 );
953
954 assert_eq!(
955 format!("{lim:#?}", lim = lim.item),
956 "ListItemMarker::RomanNumeralUpper(\n Span {\n data: \"I)\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
957 );
958
959 let lim = lim_parse("XVII) blah").unwrap();
960
961 assert_eq!(
962 lim.item,
963 ListItemMarker::RomanNumeralUpper(Span {
964 data: "XVII)",
965 line: 1,
966 col: 1,
967 offset: 0,
968 },)
969 );
970
971 assert_eq!(
972 lim.after,
973 Span {
974 data: "blah",
975 line: 1,
976 col: 7,
977 offset: 6,
978 }
979 );
980 }
981
982 #[test]
983 fn alpha_list_lower() {
984 assert!(lim_parse("a").is_none());
985 assert!(lim_parse("a)").is_none());
986
987 let lim = lim_parse("a. blah").unwrap();
988
989 assert_eq!(
990 lim.item,
991 ListItemMarker::AlphaListLower(Span {
992 data: "a.",
993 line: 1,
994 col: 1,
995 offset: 0,
996 },)
997 );
998
999 assert_eq!(
1000 lim.after,
1001 Span {
1002 data: "blah",
1003 line: 1,
1004 col: 4,
1005 offset: 3,
1006 }
1007 );
1008
1009 assert_eq!(
1010 lim.item.span(),
1011 Span {
1012 data: "a.",
1013 line: 1,
1014 col: 1,
1015 offset: 0,
1016 }
1017 );
1018
1019 assert_eq!(
1020 format!("{lim:#?}", lim = lim.item),
1021 "ListItemMarker::AlphaListLower(\n Span {\n data: \"a.\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
1022 );
1023
1024 let lim = lim_parse("x. blah").unwrap();
1025
1026 assert_eq!(
1027 lim.item,
1028 ListItemMarker::AlphaListLower(Span {
1029 data: "x.",
1030 line: 1,
1031 col: 1,
1032 offset: 0,
1033 },)
1034 );
1035
1036 assert_eq!(
1037 lim.after,
1038 Span {
1039 data: "blah",
1040 line: 1,
1041 col: 4,
1042 offset: 3,
1043 }
1044 );
1045 }
1046
1047 #[test]
1048 fn callout() {
1049 assert!(lim_parse("1> blah").is_none());
1052 assert!(lim_parse("<1>blah").is_none());
1053 assert!(lim_parse("<1>").is_none());
1054 assert!(lim_parse("<a> blah").is_none());
1055
1056 let lim = lim_parse("<1> blah").unwrap();
1057
1058 assert_eq!(
1059 lim.item,
1060 ListItemMarker::Callout(Span {
1061 data: "<1>",
1062 line: 1,
1063 col: 1,
1064 offset: 0,
1065 },)
1066 );
1067
1068 assert_eq!(
1069 lim.after,
1070 Span {
1071 data: "blah",
1072 line: 1,
1073 col: 5,
1074 offset: 4,
1075 }
1076 );
1077
1078 assert_eq!(
1079 lim.item.span(),
1080 Span {
1081 data: "<1>",
1082 line: 1,
1083 col: 1,
1084 offset: 0,
1085 }
1086 );
1087
1088 assert!(lim.item.ordinal_value().is_none());
1090 assert!(lim.item.ordinal_to_marker_text(1).is_none());
1091
1092 let lim2 = lim_parse("<.> blah").unwrap();
1095 assert_eq!(
1096 lim2.item,
1097 ListItemMarker::Callout(Span {
1098 data: "<.>",
1099 line: 1,
1100 col: 1,
1101 offset: 0,
1102 },)
1103 );
1104 assert!(lim.item.is_match_for(&lim2.item));
1105 assert!(!lim.item.is_match_for(&lim_parse("- blah").unwrap().item));
1106
1107 assert_eq!(lim.item.callout_number(), Some(1));
1110 assert!(lim2.item.callout_number().is_none());
1111 assert!(lim_parse("- blah").unwrap().item.callout_number().is_none());
1112
1113 assert_eq!(
1114 format!("{:#?}", lim.item),
1115 "ListItemMarker::Callout(\n Span {\n data: \"<1>\",\n line: 1,\n col: 1,\n offset: 0,\n },\n)"
1116 );
1117
1118 assert!(crate::blocks::ListItemMarker::starts_with_marker(
1119 crate::Span::new("<1> blah")
1120 ));
1121 assert!(!crate::blocks::ListItemMarker::starts_with_marker(
1122 crate::Span::new("1> blah")
1123 ));
1124
1125 assert!(crate::blocks::ListItemMarker::starts_with_marker(
1128 crate::Span::new(" <1> blah")
1129 ));
1130 assert!(crate::blocks::ListItemMarker::starts_with_marker(
1131 crate::Span::new(" - blah")
1132 ));
1133 }
1134}