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