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