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