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