1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::Attrlist,
6 blocks::{Block, ContentModel, IsBlock, ListItem, ListItemMarker, metadata::BlockMetadata},
7 internal::debug::DebugSliceReference,
8 span::MatchedItem,
9 strings::CowStr,
10 warnings::{Warning, WarningType},
11};
12
13#[derive(Clone, Eq, PartialEq)]
19pub struct ListBlock<'src> {
20 type_: ListType,
21 items: Vec<Block<'src>>,
22 source: Span<'src>,
23 title_source: Option<Span<'src>>,
24 title: Option<String>,
25 anchor: Option<Span<'src>>,
26 anchor_reftext: Option<Span<'src>>,
27 attrlist: Option<Attrlist<'src>>,
28 is_checklist: bool,
29 is_bibliography: bool,
30}
31
32impl<'src> ListBlock<'src> {
33 pub(crate) fn parse(
34 metadata: &BlockMetadata<'src>,
35 parser: &mut Parser,
36 warnings: &mut Vec<Warning<'src>>,
37 ) -> Option<MatchedItem<'src, Self>> {
38 Self::parse_inside_list(metadata, &[], parser, warnings)
39 }
40
41 pub(crate) fn parse_inside_list(
42 metadata: &BlockMetadata<'src>,
43 parent_list_markers: &[ListItemMarker<'src>],
44 parser: &mut Parser,
45 warnings: &mut Vec<Warning<'src>>,
46 ) -> Option<MatchedItem<'src, Self>> {
47 let source = metadata.block_start.discard_empty_lines();
48
49 let own_style_bibliography = metadata
59 .attrlist
60 .as_ref()
61 .and_then(|attrlist| attrlist.block_style())
62 == Some("bibliography");
63 let section_propagated_bibliography =
64 parent_list_markers.is_empty() && parser.parsing_bibliography_section_body;
65
66 let mut items: Vec<Block<'src>> = vec![];
67 let mut next_item_source = source;
68 let mut first_marker: Option<ListItemMarker<'src>> = None;
69 let mut expected_ordinal: Option<u32> = None;
70
71 loop {
72 let next_line_mi = next_item_source.take_normalized_line();
73
74 if next_line_mi.item.data().is_empty() || next_line_mi.item.data() == "+" {
75 if next_item_source.is_empty() || !parent_list_markers.is_empty() {
76 break;
77 } else {
78 next_item_source = next_line_mi.after;
79 continue;
80 }
81 }
82
83 let list_item_metadata = BlockMetadata {
85 title_source: None,
86 title: None,
87 anchor: None,
88 anchor_reftext: None,
89 attrlist: None,
90 source: next_item_source,
91 block_start: next_item_source,
92 };
93
94 let Some(list_item_marker_mi) =
95 ListItemMarker::parse(list_item_metadata.block_start, parser)
96 else {
97 break;
98 };
99
100 let this_item_marker = list_item_marker_mi.item;
101
102 if let Some(ref first_marker) = first_marker {
105 if !first_marker.is_match_for(&this_item_marker)
106 && parent_list_markers
107 .iter()
108 .any(|parent| parent.is_match_for(&this_item_marker))
109 {
110 break;
113 }
114
115 if let Some(actual_ordinal) = this_item_marker.ordinal_value() {
117 if let Some(expected) = expected_ordinal
118 && actual_ordinal != expected
119 {
120 if let (Some(expected_text), Some(actual_text)) = (
122 first_marker.ordinal_to_marker_text(expected),
123 first_marker.ordinal_to_marker_text(actual_ordinal),
124 ) {
125 warnings.push(Warning {
126 source: this_item_marker.span(),
127 warning: WarningType::ListItemOutOfSequence(
128 expected_text,
129 actual_text,
130 ),
131 });
132 }
133 }
134 expected_ordinal = Some(actual_ordinal + 1);
135 }
136 } else {
137 first_marker = Some(this_item_marker.clone());
138
139 if let Some(ordinal) = this_item_marker.ordinal_value() {
141 expected_ordinal = Some(ordinal + 1);
142 }
143 }
144
145 let item_is_bibliography = own_style_bibliography
151 || (section_propagated_bibliography
152 && matches!(
153 this_item_marker,
154 ListItemMarker::Asterisks(_)
155 | ListItemMarker::Hyphen(_)
156 | ListItemMarker::Bullet(_)
157 ));
158
159 let Some(list_item_mi) = ListItem::parse(
160 &list_item_metadata,
161 parent_list_markers,
162 item_is_bibliography,
163 parser,
164 warnings,
165 ) else {
166 break;
167 };
168
169 items.push(Block::ListItem(list_item_mi.item));
170 next_item_source = list_item_mi.after;
171 }
172
173 if items.is_empty() {
174 return None;
175 }
176
177 let first_marker = first_marker?;
178 let type_ = match first_marker {
179 ListItemMarker::Asterisks(_) => ListType::Unordered,
180 ListItemMarker::Hyphen(_) => ListType::Unordered,
181 ListItemMarker::Bullet(_) => ListType::Unordered,
182 ListItemMarker::Dots(_) => ListType::Ordered,
183 ListItemMarker::AlphaListCapital(_) => ListType::Ordered,
184 ListItemMarker::AlphaListLower(_) => ListType::Ordered,
185 ListItemMarker::RomanNumeralLower(_) => ListType::Ordered,
186 ListItemMarker::RomanNumeralUpper(_) => ListType::Ordered,
187 ListItemMarker::ArabicNumeral(_) => ListType::Ordered,
188 ListItemMarker::Callout(_) => ListType::Callout,
189
190 ListItemMarker::DefinedTerm {
191 term: _,
192 marker: _,
193 source: _,
194 } => ListType::Description,
195 };
196
197 if type_ == ListType::Callout {
205 for (index, item) in items.iter().enumerate() {
206 let position = (index + 1) as u32;
207
208 if let Some(marker_number) = item
209 .as_list_item()
210 .and_then(|li| li.list_item_marker().callout_number())
211 && marker_number != position
212 {
213 warnings.push(Warning {
214 source: item.span(),
215 warning: WarningType::CalloutListItemOutOfSequence(
216 position as usize,
217 marker_number as usize,
218 ),
219 });
220 }
221
222 if !parser.callout_defined(position) {
223 warnings.push(Warning {
224 source: item.span(),
225 warning: WarningType::NoCalloutFound(position as usize),
226 });
227 }
228 }
229 parser.close_callout_list();
230 }
231
232 let is_checklist = type_ == ListType::Unordered
236 && items.iter().any(|item| {
237 item.as_list_item()
238 .is_some_and(|li| li.checkbox().is_some())
239 });
240
241 let is_bibliography = own_style_bibliography
244 || (section_propagated_bibliography && type_ == ListType::Unordered);
245
246 Some(MatchedItem {
247 item: Self {
248 type_,
249 items,
250 source: metadata
251 .source
252 .trim_remainder(next_item_source)
253 .trim_trailing_line_end()
254 .trim_trailing_whitespace(),
255 title_source: metadata.title_source,
256 title: metadata.title.clone(),
257 anchor: metadata.anchor,
258 anchor_reftext: metadata.anchor_reftext,
259 attrlist: metadata.attrlist.clone(),
260 is_checklist,
261 is_bibliography,
262 },
263 after: next_item_source,
264 })
265 }
266
267 pub fn type_(&self) -> ListType {
269 self.type_
270 }
271
272 pub fn is_checklist(&self) -> bool {
280 self.is_checklist
281 }
282
283 pub fn is_bibliography(&self) -> bool {
292 self.is_bibliography
293 }
294
295 pub fn marker_style(&self) -> Option<&'static str> {
303 let first_marker = self.items.first()?.as_list_item()?.list_item_marker();
304
305 match first_marker {
306 ListItemMarker::Dots(span) => {
307 let marker_len = span.data().len();
308 match marker_len {
309 1 => Some("arabic"),
310 2 => Some("loweralpha"),
311 3 => Some("lowerroman"),
312 4 => Some("upperalpha"),
313 5 => Some("upperroman"),
314 _ => Some("arabic"),
315 }
316 }
317 ListItemMarker::ArabicNumeral(_) => Some("arabic"),
318 ListItemMarker::Callout(_) => Some("arabic"),
319 ListItemMarker::AlphaListLower(_) => Some("loweralpha"),
320 ListItemMarker::AlphaListCapital(_) => Some("upperalpha"),
321 ListItemMarker::RomanNumeralLower(_) => Some("lowerroman"),
322 ListItemMarker::RomanNumeralUpper(_) => Some("upperroman"),
323 _ => None,
324 }
325 }
326
327 pub fn start(&self) -> Option<i64> {
345 if self.type_ != ListType::Ordered {
346 return None;
347 }
348
349 let resolved = self
352 .attrlist
353 .as_ref()
354 .and_then(|attrlist| attrlist.named_attribute("start"))
355 .and_then(|attr| attr.value().trim().parse::<i64>().ok())
356 .or_else(|| {
357 self.items
358 .first()
359 .and_then(|item| item.as_list_item())
360 .and_then(|li| li.list_item_marker().ordinal_value())
361 .map(i64::from)
362 });
363
364 resolved.filter(|&n| n != 1)
367 }
368}
369
370impl<'src> IsBlock<'src> for ListBlock<'src> {
371 fn content_model(&self) -> ContentModel {
372 ContentModel::Compound
373 }
374
375 fn raw_context(&self) -> CowStr<'src> {
376 "list".into()
377 }
378
379 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
380 self.items.iter()
381 }
382
383 fn nested_blocks_mut(&mut self) -> &mut [Block<'src>] {
384 &mut self.items
385 }
386
387 fn title_source(&'src self) -> Option<Span<'src>> {
388 self.title_source
389 }
390
391 fn title(&self) -> Option<&str> {
392 self.title.as_deref()
393 }
394
395 fn anchor(&'src self) -> Option<Span<'src>> {
396 self.anchor
397 }
398
399 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
400 self.anchor_reftext
401 }
402
403 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
404 self.attrlist.as_ref()
405 }
406}
407
408impl<'src> HasSpan<'src> for ListBlock<'src> {
409 fn span(&self) -> Span<'src> {
410 self.source
411 }
412}
413
414impl std::fmt::Debug for ListBlock<'_> {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 f.debug_struct("ListBlock")
417 .field("type_", &self.type_)
418 .field("items", &DebugSliceReference(&self.items))
419 .field("source", &self.source)
420 .field("title_source", &self.title_source)
421 .field("title", &self.title)
422 .field("anchor", &self.anchor)
423 .field("anchor_reftext", &self.anchor_reftext)
424 .field("attrlist", &self.attrlist)
425 .field("is_checklist", &self.is_checklist)
426 .field("is_bibliography", &self.is_bibliography)
427 .finish()
428 }
429}
430
431#[derive(Clone, Copy, Eq, PartialEq)]
433pub enum ListType {
434 Unordered,
437
438 Ordered,
441
442 Description,
445
446 Callout,
450}
451
452impl std::fmt::Debug for ListType {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 match self {
455 ListType::Unordered => write!(f, "ListType::Unordered"),
456 ListType::Ordered => write!(f, "ListType::Ordered"),
457 ListType::Description => write!(f, "ListType::Description"),
458 ListType::Callout => write!(f, "ListType::Callout"),
459 }
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 #![allow(clippy::indexing_slicing)]
466 #![allow(clippy::panic)]
467 #![allow(clippy::unwrap_used)]
468
469 use crate::{
470 blocks::{ContentModel, ListType, metadata::BlockMetadata},
471 span::MatchedItem,
472 tests::prelude::*,
473 warnings::Warning,
474 };
475
476 fn list_parse<'a>(source: &'a str) -> Option<MatchedItem<'a, crate::blocks::ListBlock<'a>>> {
477 let mut parser = crate::Parser::default();
478 let mut warnings: Vec<Warning<'a>> = vec![];
479
480 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
481
482 let result = crate::blocks::list::ListBlock::parse(&metadata, &mut parser, &mut warnings);
483
484 assert!(warnings.is_empty());
485
486 result
487 }
488
489 fn list_parse_with_warnings<'a>(
493 source: &'a str,
494 ) -> (
495 Option<MatchedItem<'a, crate::blocks::ListBlock<'a>>>,
496 Vec<Warning<'a>>,
497 ) {
498 let mut parser = crate::Parser::default();
499 let mut warnings: Vec<Warning<'a>> = vec![];
500
501 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
502
503 let result = crate::blocks::list::ListBlock::parse(&metadata, &mut parser, &mut warnings);
504
505 (result, warnings)
506 }
507
508 #[test]
509 fn basic_case() {
510 assert!(list_parse("-xyz").is_none());
511 assert!(list_parse("-- x").is_none());
512
513 let list = list_parse("- blah").unwrap();
514
515 assert_eq!(
516 list.item,
517 ListBlock {
518 type_: ListType::Unordered,
519 items: &[Block::ListItem(ListItem {
520 marker: ListItemMarker::Hyphen(Span {
521 data: "-",
522 line: 1,
523 col: 1,
524 offset: 0,
525 },),
526 blocks: &[Block::Simple(SimpleBlock {
527 content: Content {
528 original: Span {
529 data: "blah",
530 line: 1,
531 col: 3,
532 offset: 2,
533 },
534 rendered: "blah",
535 },
536 source: Span {
537 data: "blah",
538 line: 1,
539 col: 3,
540 offset: 2,
541 },
542 style: SimpleBlockStyle::Paragraph,
543 title_source: None,
544 title: None,
545 caption: None,
546 number: None,
547 anchor: None,
548 anchor_reftext: None,
549 attrlist: None,
550 },),],
551 source: Span {
552 data: "- blah",
553 line: 1,
554 col: 1,
555 offset: 0,
556 },
557 anchor: None,
558 anchor_reftext: None,
559 attrlist: None,
560 },),],
561 source: Span {
562 data: "- blah",
563 line: 1,
564 col: 1,
565 offset: 0,
566 },
567 title_source: None,
568 title: None,
569 anchor: None,
570 anchor_reftext: None,
571 attrlist: None,
572 }
573 );
574
575 assert_eq!(list.item.type_(), ListType::Unordered);
576 assert_eq!(list.item.content_model(), ContentModel::Compound);
577 assert_eq!(list.item.raw_context().as_ref(), "list");
578
579 let mut list_blocks = list.item.nested_blocks();
580
581 let list_item = list_blocks.next().unwrap();
582
583 assert_eq!(
584 list_item,
585 &Block::ListItem(ListItem {
586 marker: ListItemMarker::Hyphen(Span {
587 data: "-",
588 line: 1,
589 col: 1,
590 offset: 0,
591 },),
592 blocks: &[Block::Simple(SimpleBlock {
593 content: Content {
594 original: Span {
595 data: "blah",
596 line: 1,
597 col: 3,
598 offset: 2,
599 },
600 rendered: "blah",
601 },
602 source: Span {
603 data: "blah",
604 line: 1,
605 col: 3,
606 offset: 2,
607 },
608 style: SimpleBlockStyle::Paragraph,
609 title_source: None,
610 title: None,
611 caption: None,
612 number: None,
613 anchor: None,
614 anchor_reftext: None,
615 attrlist: None,
616 },),],
617 source: Span {
618 data: "- blah",
619 line: 1,
620 col: 1,
621 offset: 0,
622 },
623 anchor: None,
624 anchor_reftext: None,
625 attrlist: None,
626 })
627 );
628
629 assert_eq!(list_item.content_model(), ContentModel::Compound);
630 assert_eq!(list_item.raw_context().as_ref(), "list_item");
631
632 let mut li_blocks = list_item.nested_blocks();
633
634 assert_eq!(
635 li_blocks.next().unwrap(),
636 &Block::Simple(SimpleBlock {
637 content: Content {
638 original: Span {
639 data: "blah",
640 line: 1,
641 col: 3,
642 offset: 2,
643 },
644 rendered: "blah",
645 },
646 source: Span {
647 data: "blah",
648 line: 1,
649 col: 3,
650 offset: 2,
651 },
652 style: SimpleBlockStyle::Paragraph,
653 title_source: None,
654 title: None,
655 caption: None,
656 number: None,
657 anchor: None,
658 anchor_reftext: None,
659 attrlist: None,
660 })
661 );
662 assert!(li_blocks.next().is_none());
663
664 assert!(list_item.title_source().is_none());
665 assert!(list_item.title().is_none());
666 assert!(list_item.anchor().is_none());
667 assert!(list_item.anchor_reftext().is_none());
668 assert!(list_item.attrlist().is_none());
669 assert_eq!(list_item.substitution_group(), SubstitutionGroup::Normal);
670 assert_eq!(
671 list_item.span(),
672 Span {
673 data: "- blah",
674 line: 1,
675 col: 1,
676 offset: 0,
677 }
678 );
679
680 assert!(list_blocks.next().is_none());
681
682 assert!(list.item.title_source().is_none());
683 assert!(list.item.title().is_none());
684 assert!(list.item.anchor().is_none());
685 assert!(list.item.anchor_reftext().is_none());
686 assert!(list.item.attrlist().is_none());
687
688 assert_eq!(
689 format!("{:#?}", list.item),
690 "ListBlock {\n type_: ListType::Unordered,\n items: &[\n Block::ListItem(\n ListItem {\n marker: ListItemMarker::Hyphen(\n Span {\n data: \"-\",\n line: 1,\n col: 1,\n offset: 0,\n },\n ),\n blocks: &[\n Block::Simple(\n SimpleBlock {\n content: Content {\n original: Span {\n data: \"blah\",\n line: 1,\n col: 3,\n offset: 2,\n },\n rendered: \"blah\",\n },\n source: Span {\n data: \"blah\",\n line: 1,\n col: 3,\n offset: 2,\n },\n style: SimpleBlockStyle::Paragraph,\n title_source: None,\n title: None,\n caption: None,\n number: None,\n anchor: None,\n anchor_reftext: None,\n attrlist: None,\n },\n ),\n ],\n source: Span {\n data: \"- blah\",\n line: 1,\n col: 1,\n offset: 0,\n },\n anchor: None,\n anchor_reftext: None,\n attrlist: None,\n checkbox: None,\n },\n ),\n ],\n source: Span {\n data: \"- blah\",\n line: 1,\n col: 1,\n offset: 0,\n },\n title_source: None,\n title: None,\n anchor: None,\n anchor_reftext: None,\n attrlist: None,\n is_checklist: false,\n is_bibliography: false,\n}"
691 );
692
693 assert_eq!(
694 list.after,
695 Span {
696 data: "",
697 line: 1,
698 col: 7,
699 offset: 6,
700 }
701 );
702 }
703
704 #[test]
705 fn list_type_impl_debug() {
706 assert_eq!(format!("{:#?}", ListType::Unordered), "ListType::Unordered");
707 assert_eq!(format!("{:#?}", ListType::Ordered), "ListType::Ordered");
708
709 assert_eq!(
710 format!("{:#?}", ListType::Description),
711 "ListType::Description"
712 );
713
714 assert_eq!(format!("{:#?}", ListType::Callout), "ListType::Callout");
715 }
716
717 #[test]
718 fn callout_list() {
719 let (list, warnings) = list_parse_with_warnings("<1> First\n<2> Second\n");
722 let list = list.unwrap();
723
724 assert_eq!(list.item.type_(), ListType::Callout);
725 assert_eq!(list.item.marker_style(), Some("arabic"));
726
727 let items: Vec<_> = list.item.nested_blocks().collect();
728 assert_eq!(items.len(), 2);
729
730 assert_eq!(
731 items[0].nested_blocks().next().unwrap().rendered_content(),
732 Some("First")
733 );
734 assert_eq!(
735 items[1].nested_blocks().next().unwrap().rendered_content(),
736 Some("Second")
737 );
738
739 let warning_types: Vec<_> = warnings.iter().map(|w| &w.warning).collect();
740 assert_eq!(
741 warning_types,
742 vec![
743 &WarningType::NoCalloutFound(1),
744 &WarningType::NoCalloutFound(2),
745 ]
746 );
747 }
748
749 #[test]
750 fn callout_list_auto_numbered() {
751 let (list, warnings) = list_parse_with_warnings("<.> First\n<.> Second\n<.> Third\n");
753 let list = list.unwrap();
754
755 assert_eq!(list.item.type_(), ListType::Callout);
756 assert_eq!(list.item.nested_blocks().count(), 3);
757
758 assert_eq!(warnings.len(), 3);
760 }
761
762 #[test]
763 fn callout_list_marker_only_trailing_bracket_is_not_a_list() {
764 assert!(list_parse("1> Not a callout list item\n").is_none());
766 }
767
768 #[test]
769 fn attrlist_doesnt_exit() {
770 let list = list_parse("* Foo\n[loweralpha]\n. Boo\n* Blech").unwrap();
771
772 assert_eq!(
773 list.item,
774 ListBlock {
775 type_: ListType::Unordered,
776 items: &[
777 Block::ListItem(ListItem {
778 marker: ListItemMarker::Asterisks(Span {
779 data: "*",
780 line: 1,
781 col: 1,
782 offset: 0,
783 },),
784 blocks: &[
785 Block::Simple(SimpleBlock {
786 content: Content {
787 original: Span {
788 data: "Foo",
789 line: 1,
790 col: 3,
791 offset: 2,
792 },
793 rendered: "Foo",
794 },
795 source: Span {
796 data: "Foo",
797 line: 1,
798 col: 3,
799 offset: 2,
800 },
801 style: SimpleBlockStyle::Paragraph,
802 title_source: None,
803 title: None,
804 caption: None,
805 number: None,
806 anchor: None,
807 anchor_reftext: None,
808 attrlist: None,
809 },),
810 Block::List(ListBlock {
811 type_: ListType::Ordered,
812 items: &[Block::ListItem(ListItem {
813 marker: ListItemMarker::Dots(Span {
814 data: ".",
815 line: 3,
816 col: 1,
817 offset: 19,
818 },),
819 blocks: &[Block::Simple(SimpleBlock {
820 content: Content {
821 original: Span {
822 data: "Boo",
823 line: 3,
824 col: 3,
825 offset: 21,
826 },
827 rendered: "Boo",
828 },
829 source: Span {
830 data: "Boo",
831 line: 3,
832 col: 3,
833 offset: 21,
834 },
835 style: SimpleBlockStyle::Paragraph,
836 title_source: None,
837 title: None,
838 caption: None,
839 number: None,
840 anchor: None,
841 anchor_reftext: None,
842 attrlist: None,
843 },),],
844 source: Span {
845 data: ". Boo",
846 line: 3,
847 col: 1,
848 offset: 19,
849 },
850 anchor: None,
851 anchor_reftext: None,
852 attrlist: None,
853 },),],
854 source: Span {
855 data: "[loweralpha]\n. Boo",
856 line: 2,
857 col: 1,
858 offset: 6,
859 },
860 title_source: None,
861 title: None,
862 anchor: None,
863 anchor_reftext: None,
864 attrlist: Some(Attrlist {
865 attributes: &[ElementAttribute {
866 name: None,
867 value: "loweralpha",
868 shorthand_items: &["loweralpha"],
869 },],
870 anchor: None,
871 source: Span {
872 data: "loweralpha",
873 line: 2,
874 col: 2,
875 offset: 7,
876 },
877 },),
878 },),
879 ],
880 source: Span {
881 data: "* Foo\n[loweralpha]\n. Boo",
882 line: 1,
883 col: 1,
884 offset: 0,
885 },
886 anchor: None,
887 anchor_reftext: None,
888 attrlist: None,
889 },),
890 Block::ListItem(ListItem {
891 marker: ListItemMarker::Asterisks(Span {
892 data: "*",
893 line: 4,
894 col: 1,
895 offset: 25,
896 },),
897 blocks: &[Block::Simple(SimpleBlock {
898 content: Content {
899 original: Span {
900 data: "Blech",
901 line: 4,
902 col: 3,
903 offset: 27,
904 },
905 rendered: "Blech",
906 },
907 source: Span {
908 data: "Blech",
909 line: 4,
910 col: 3,
911 offset: 27,
912 },
913 style: SimpleBlockStyle::Paragraph,
914 title_source: None,
915 title: None,
916 caption: None,
917 number: None,
918 anchor: None,
919 anchor_reftext: None,
920 attrlist: None,
921 },),],
922 source: Span {
923 data: "* Blech",
924 line: 4,
925 col: 1,
926 offset: 25,
927 },
928 anchor: None,
929 anchor_reftext: None,
930 attrlist: None,
931 },),
932 ],
933 source: Span {
934 data: "* Foo\n[loweralpha]\n. Boo\n* Blech",
935 line: 1,
936 col: 1,
937 offset: 0,
938 },
939 title_source: None,
940 title: None,
941 anchor: None,
942 anchor_reftext: None,
943 attrlist: None,
944 }
945 );
946
947 assert_eq!(
948 list.after,
949 Span {
950 data: "",
951 line: 4,
952 col: 8,
953 offset: 32,
954 }
955 );
956 }
957
958 #[test]
959 fn metadata_merged_across_empty_lines_for_nested_list() {
960 let list = list_parse("* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo\n* Blech").unwrap();
963
964 assert_eq!(
965 list.item,
966 ListBlock {
967 type_: ListType::Unordered,
968 items: &[
969 Block::ListItem(ListItem {
970 marker: ListItemMarker::Asterisks(Span {
971 data: "*",
972 line: 1,
973 col: 1,
974 offset: 0,
975 },),
976 blocks: &[
977 Block::Simple(SimpleBlock {
978 content: Content {
979 original: Span {
980 data: "Foo",
981 line: 1,
982 col: 3,
983 offset: 2,
984 },
985 rendered: "Foo",
986 },
987 source: Span {
988 data: "Foo",
989 line: 1,
990 col: 3,
991 offset: 2,
992 },
993 style: SimpleBlockStyle::Paragraph,
994 title_source: None,
995 title: None,
996 caption: None,
997 number: None,
998 anchor: None,
999 anchor_reftext: None,
1000 attrlist: None,
1001 },),
1002 Block::List(ListBlock {
1003 type_: ListType::Ordered,
1004 items: &[Block::ListItem(ListItem {
1005 marker: ListItemMarker::Dots(Span {
1006 data: ".",
1007 line: 5,
1008 col: 1,
1009 offset: 31,
1010 },),
1011 blocks: &[Block::Simple(SimpleBlock {
1012 content: Content {
1013 original: Span {
1014 data: "Boo",
1015 line: 5,
1016 col: 3,
1017 offset: 33,
1018 },
1019 rendered: "Boo",
1020 },
1021 source: Span {
1022 data: "Boo",
1023 line: 5,
1024 col: 3,
1025 offset: 33,
1026 },
1027 style: SimpleBlockStyle::Paragraph,
1028 title_source: None,
1029 title: None,
1030 caption: None,
1031 number: None,
1032 anchor: None,
1033 anchor_reftext: None,
1034 attrlist: None,
1035 },),],
1036 source: Span {
1037 data: ". Boo",
1038 line: 5,
1039 col: 1,
1040 offset: 31,
1041 },
1042 anchor: None,
1043 anchor_reftext: None,
1044 attrlist: None,
1045 },),],
1046 source: Span {
1047 data: "[loweralpha]\n\n[[anchor]]\n. Boo",
1048 line: 2,
1049 col: 1,
1050 offset: 6,
1051 },
1052 title_source: None,
1053 title: None,
1054 anchor: Some(Span {
1055 data: "anchor",
1056 line: 4,
1057 col: 3,
1058 offset: 22,
1059 },),
1060 anchor_reftext: None,
1061 attrlist: Some(Attrlist {
1062 attributes: &[ElementAttribute {
1063 name: None,
1064 value: "loweralpha",
1065 shorthand_items: &["loweralpha"],
1066 },],
1067 anchor: None,
1068 source: Span {
1069 data: "loweralpha",
1070 line: 2,
1071 col: 2,
1072 offset: 7,
1073 },
1074 },),
1075 },),
1076 ],
1077 source: Span {
1078 data: "* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo",
1079 line: 1,
1080 col: 1,
1081 offset: 0,
1082 },
1083 anchor: None,
1084 anchor_reftext: None,
1085 attrlist: None,
1086 },),
1087 Block::ListItem(ListItem {
1088 marker: ListItemMarker::Asterisks(Span {
1089 data: "*",
1090 line: 6,
1091 col: 1,
1092 offset: 37,
1093 },),
1094 blocks: &[Block::Simple(SimpleBlock {
1095 content: Content {
1096 original: Span {
1097 data: "Blech",
1098 line: 6,
1099 col: 3,
1100 offset: 39,
1101 },
1102 rendered: "Blech",
1103 },
1104 source: Span {
1105 data: "Blech",
1106 line: 6,
1107 col: 3,
1108 offset: 39,
1109 },
1110 style: SimpleBlockStyle::Paragraph,
1111 title_source: None,
1112 title: None,
1113 caption: None,
1114 number: None,
1115 anchor: None,
1116 anchor_reftext: None,
1117 attrlist: None,
1118 },),],
1119 source: Span {
1120 data: "* Blech",
1121 line: 6,
1122 col: 1,
1123 offset: 37,
1124 },
1125 anchor: None,
1126 anchor_reftext: None,
1127 attrlist: None,
1128 },),
1129 ],
1130 source: Span {
1131 data: "* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo\n* Blech",
1132 line: 1,
1133 col: 1,
1134 offset: 0,
1135 },
1136 title_source: None,
1137 title: None,
1138 anchor: None,
1139 anchor_reftext: None,
1140 attrlist: None,
1141 }
1142 );
1143 }
1144
1145 #[test]
1146 fn parent_marker_after_metadata_separated_by_empty_lines() {
1147 let list =
1154 list_parse("* grandparent\n** parent\n*** nested\n[[anchor]]\n\n* back to grandparent")
1155 .unwrap();
1156
1157 assert_eq!(list.item.nested_blocks().count(), 2);
1159 assert_eq!(list.item.type_(), ListType::Unordered);
1160
1161 let mut outer_items = list.item.nested_blocks();
1162
1163 let first_outer = outer_items.next().unwrap();
1165 let first_outer_blocks: Vec<_> = first_outer.nested_blocks().collect();
1166 assert_eq!(first_outer_blocks.len(), 2); let nested_list = &first_outer_blocks[1];
1170 assert_eq!(nested_list.nested_blocks().count(), 1);
1171
1172 let parent_item = nested_list.nested_blocks().next().unwrap();
1174 let parent_blocks: Vec<_> = parent_item.nested_blocks().collect();
1175 assert_eq!(parent_blocks.len(), 2); let innermost_list = &parent_blocks[1];
1179 assert_eq!(innermost_list.nested_blocks().count(), 1);
1180
1181 let innermost_item = innermost_list.nested_blocks().next().unwrap();
1183 assert_eq!(innermost_item.nested_blocks().count(), 1);
1184
1185 let second_outer = outer_items.next().unwrap();
1187 assert_eq!(second_outer.nested_blocks().count(), 1);
1188 assert!(outer_items.next().is_none());
1189 }
1190
1191 #[test]
1192 fn marker_style_single_dot() {
1193 let list = list_parse(". Item one\n. Item two\n").unwrap();
1194 assert_eq!(list.item.marker_style(), Some("arabic"));
1195 }
1196
1197 #[test]
1198 fn marker_style_double_dots() {
1199 let list = list_parse(".. Item a\n.. Item b\n").unwrap();
1200 assert_eq!(list.item.marker_style(), Some("loweralpha"));
1201 }
1202
1203 #[test]
1204 fn marker_style_triple_dots() {
1205 let list = list_parse("... Item i\n... Item ii\n").unwrap();
1206 assert_eq!(list.item.marker_style(), Some("lowerroman"));
1207 }
1208
1209 #[test]
1210 fn marker_style_four_dots() {
1211 let list = list_parse(".... Item A\n.... Item B\n").unwrap();
1212 assert_eq!(list.item.marker_style(), Some("upperalpha"));
1213 }
1214
1215 #[test]
1216 fn marker_style_five_dots() {
1217 let list = list_parse("..... Item I\n..... Item II\n").unwrap();
1218 assert_eq!(list.item.marker_style(), Some("upperroman"));
1219 }
1220
1221 #[test]
1222 fn marker_style_hyphen_returns_none() {
1223 let list = list_parse("- Item one\n- Item two\n").unwrap();
1224 assert_eq!(list.item.marker_style(), None);
1225 }
1226
1227 #[test]
1228 fn marker_style_asterisk_returns_none() {
1229 let list = list_parse("* Item one\n* Item two\n").unwrap();
1230 assert_eq!(list.item.marker_style(), None);
1231 }
1232
1233 #[test]
1234 fn marker_with_no_content() {
1235 assert!(list_parse("- ").is_none());
1239 assert!(list_parse("* ").is_none());
1240 assert!(list_parse(". ").is_none());
1241 }
1242
1243 #[test]
1244 fn orphaned_title_after_continuation_is_discarded() {
1245 let list = list_parse("* item one\n+\n.Title\n\nsecond paragraph").unwrap();
1252
1253 let mut items = list.item.nested_blocks();
1255 let item = items.next().unwrap();
1256 assert!(items.next().is_none());
1257
1258 let blocks: Vec<_> = item.nested_blocks().collect();
1261 assert_eq!(blocks.len(), 2);
1262
1263 assert_eq!(
1265 blocks[0],
1266 &Block::Simple(SimpleBlock {
1267 content: Content {
1268 original: Span {
1269 data: "item one",
1270 line: 1,
1271 col: 3,
1272 offset: 2,
1273 },
1274 rendered: "item one",
1275 },
1276 source: Span {
1277 data: "item one",
1278 line: 1,
1279 col: 3,
1280 offset: 2,
1281 },
1282 style: SimpleBlockStyle::Paragraph,
1283 title_source: None,
1284 title: None,
1285 caption: None,
1286 number: None,
1287 anchor: None,
1288 anchor_reftext: None,
1289 attrlist: None,
1290 })
1291 );
1292
1293 assert_eq!(
1295 blocks[1],
1296 &Block::Simple(SimpleBlock {
1297 content: Content {
1298 original: Span {
1299 data: "second paragraph",
1300 line: 5,
1301 col: 1,
1302 offset: 21,
1303 },
1304 rendered: "second paragraph",
1305 },
1306 source: Span {
1307 data: "second paragraph",
1308 line: 5,
1309 col: 1,
1310 offset: 21,
1311 },
1312 style: SimpleBlockStyle::Paragraph,
1313 title_source: None,
1314 title: None,
1315 caption: None,
1316 number: None,
1317 anchor: None,
1318 anchor_reftext: None,
1319 attrlist: None,
1320 })
1321 );
1322 }
1323
1324 #[test]
1325 fn block_list_enum_case() {
1326 let mut parser = crate::Parser::default();
1327
1328 let mi = crate::blocks::Block::parse(crate::Span::new("- blah"), &mut parser)
1329 .unwrap_if_no_warnings()
1330 .unwrap();
1331
1332 assert!(matches!(mi.item, crate::blocks::Block::List(_)));
1333
1334 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1335 assert!(mi.item.rendered_content().is_none());
1336 assert_eq!(mi.item.raw_context().as_ref(), "list");
1337 assert_eq!(mi.item.nested_blocks().count(), 1);
1338 assert!(mi.item.title_source().is_none());
1339 assert!(mi.item.title().is_none());
1340 assert!(mi.item.anchor().is_none());
1341 assert!(mi.item.anchor_reftext().is_none());
1342 assert!(mi.item.attrlist().is_none());
1343 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1344
1345 assert_eq!(
1346 mi.item.span(),
1347 Span {
1348 data: "- blah",
1349 line: 1,
1350 col: 1,
1351 offset: 0,
1352 }
1353 );
1354
1355 let debug_str = format!("{:?}", mi.item);
1356 assert!(debug_str.starts_with("Block::List("));
1357 }
1358
1359 mod start {
1360 use super::list_parse;
1361 use crate::blocks::ListType;
1362
1363 #[test]
1364 fn unordered_list_has_no_start() {
1365 let mi = list_parse("* one\n* two").unwrap();
1366 assert_eq!(mi.item.type_(), ListType::Unordered);
1367 assert_eq!(mi.item.start(), None);
1368 }
1369
1370 #[test]
1371 fn implicit_ordered_marker_has_no_start() {
1372 let mi = list_parse(". one\n. two").unwrap();
1375 assert_eq!(mi.item.type_(), ListType::Ordered);
1376 assert_eq!(mi.item.start(), None);
1377 }
1378
1379 #[test]
1380 fn explicit_arabic_first_marker_sets_start() {
1381 let mi = list_parse("7. one\n8. two").unwrap();
1382 assert_eq!(mi.item.type_(), ListType::Ordered);
1383 assert_eq!(mi.item.start(), Some(7));
1384 }
1385
1386 #[test]
1387 fn explicit_alpha_first_marker_sets_start() {
1388 let mi = list_parse("c. one\nd. two").unwrap();
1390 assert_eq!(mi.item.start(), Some(3));
1391 }
1392
1393 #[test]
1394 fn explicit_ordinal_one_marker_defaults_to_none() {
1395 assert_eq!(list_parse("1. one\n2. two").unwrap().item.start(), None);
1399 assert_eq!(list_parse("a. one\nb. two").unwrap().item.start(), None);
1400 }
1401
1402 #[test]
1403 fn start_attribute_takes_precedence() {
1404 let mi = list_parse("[start=5]\n. one\n. two").unwrap();
1405 assert_eq!(mi.item.type_(), ListType::Ordered);
1406 assert_eq!(mi.item.start(), Some(5));
1407 }
1408
1409 #[test]
1410 fn start_attribute_of_one_is_none() {
1411 let mi = list_parse("[start=1]\n. one\n. two").unwrap();
1413 assert_eq!(mi.item.start(), None);
1414 }
1415
1416 #[test]
1417 fn start_attribute_overrides_first_marker() {
1418 let mi = list_parse("[start=5]\n7. one\n8. two").unwrap();
1419 assert_eq!(mi.item.start(), Some(5));
1420 }
1421
1422 #[test]
1423 fn non_numeric_start_attribute_falls_back_to_marker() {
1424 let mi = list_parse("[start=abc]\n7. one\n8. two").unwrap();
1425 assert_eq!(mi.item.start(), Some(7));
1426 }
1427 }
1428}