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 declared_style = metadata
88 .attrlist
89 .as_ref()
90 .and_then(|attrlist| attrlist.block_style());
91
92 let own_style_bibliography = declared_style == Some("bibliography");
93 let section_propagated_bibliography = parent_list_markers.is_empty()
94 && declared_style.is_none()
95 && parser.parsing_bibliography_section_body;
96
97 let mut items: Vec<Block<'src>> = vec![];
98 let mut next_item_source = source;
99 let mut first_marker: Option<ListItemMarker<'src>> = None;
100 let mut expected_ordinal: Option<u32> = None;
101
102 loop {
103 let next_line_mi = next_item_source.take_normalized_line();
104
105 if next_line_mi.item.data().is_empty() {
111 break;
112 }
113
114 if next_line_mi.item.data() == "+" {
117 if next_item_source.is_empty() || !parent_list_markers.is_empty() {
118 break;
119 } else {
120 next_item_source = next_line_mi.after;
121 continue;
122 }
123 }
124
125 let (list_item_metadata, mut list_item_metadata_warnings) = if first_marker.is_none() {
143 (
144 BlockMetadata {
145 title_source: None,
146 title: None,
147 anchor: None,
148 anchor_reftext: None,
149 attrlist: None,
150 source: next_item_source,
151 block_start: next_item_source,
152 },
153 vec![],
154 )
155 } else {
156 let maw = BlockMetadata::parse(next_item_source, parser);
157 (maw.item, maw.warnings)
158 };
159
160 let Some(list_item_marker_mi) =
161 ListItemMarker::parse(list_item_metadata.block_start, parser)
162 else {
163 break;
164 };
165
166 let this_item_marker = list_item_marker_mi.item;
167
168 if let Some(ref first_marker) = first_marker {
171 if !first_marker.is_match_for(&this_item_marker)
172 && parent_list_markers
173 .iter()
174 .any(|parent| parent.is_match_for(&this_item_marker))
175 {
176 break;
179 }
180
181 if let Some(actual_ordinal) = this_item_marker.ordinal_value() {
183 if let Some(expected) = expected_ordinal
184 && actual_ordinal != expected
185 {
186 if let (Some(expected_text), Some(actual_text)) = (
188 first_marker.ordinal_to_marker_text(expected),
189 first_marker.ordinal_to_marker_text(actual_ordinal),
190 ) {
191 warnings.push(Warning::new(
192 this_item_marker.span(),
193 WarningType::ListItemOutOfSequence(expected_text, actual_text),
194 ));
195 }
196 }
197 expected_ordinal = Some(actual_ordinal + 1);
198 }
199 } else {
200 first_marker = Some(this_item_marker.clone());
201
202 if let Some(ordinal) = this_item_marker.ordinal_value() {
204 expected_ordinal = Some(ordinal + 1);
205 }
206 }
207
208 let item_is_bibliography = own_style_bibliography
214 || (section_propagated_bibliography
215 && matches!(
216 this_item_marker,
217 ListItemMarker::Asterisks(_)
218 | ListItemMarker::Hyphen(_)
219 | ListItemMarker::Bullet(_)
220 ));
221
222 let Some(list_item_mi) = ListItem::parse(
223 &list_item_metadata,
224 parent_list_markers,
225 item_is_bibliography,
226 parser,
227 warnings,
228 ) else {
229 break;
230 };
231
232 warnings.append(&mut list_item_metadata_warnings);
235
236 items.push(Block::ListItem(list_item_mi.item));
237 next_item_source = list_item_mi.after;
238 }
239
240 if items.is_empty() {
241 return None;
242 }
243
244 let first_marker = first_marker?;
245 let type_ = match first_marker {
246 ListItemMarker::Asterisks(_) => ListType::Unordered,
247 ListItemMarker::Hyphen(_) => ListType::Unordered,
248 ListItemMarker::Bullet(_) => ListType::Unordered,
249 ListItemMarker::Dots(_) => ListType::Ordered,
250 ListItemMarker::AlphaListCapital(_) => ListType::Ordered,
251 ListItemMarker::AlphaListLower(_) => ListType::Ordered,
252 ListItemMarker::RomanNumeralLower(_) => ListType::Ordered,
253 ListItemMarker::RomanNumeralUpper(_) => ListType::Ordered,
254 ListItemMarker::ArabicNumeral(_) => ListType::Ordered,
255 ListItemMarker::Callout(_) => ListType::Callout,
256
257 ListItemMarker::DefinedTerm {
258 term: _,
259 marker: _,
260 source: _,
261 } => ListType::Description,
262 };
263
264 if type_ == ListType::Callout {
272 for (index, item) in items.iter().enumerate() {
273 let position = (index + 1) as u32;
274
275 if let Some(marker_number) = item
276 .as_list_item()
277 .and_then(|li| li.list_item_marker().callout_number())
278 && marker_number != position
279 {
280 warnings.push(Warning::new(
281 item.span(),
282 WarningType::CalloutListItemOutOfSequence(
283 position as usize,
284 marker_number as usize,
285 ),
286 ));
287 }
288
289 if !parser.callout_defined(position) {
290 warnings.push(Warning::new(
291 item.span(),
292 WarningType::NoCalloutFound(position as usize),
293 ));
294 }
295 }
296 parser.close_callout_list();
297 }
298
299 let is_checklist = type_ == ListType::Unordered
303 && items.iter().any(|item| {
304 item.as_list_item()
305 .is_some_and(|li| li.checkbox().is_some())
306 });
307
308 let is_bibliography = own_style_bibliography
311 || (section_propagated_bibliography && type_ == ListType::Unordered);
312
313 Some(MatchedItem {
314 item: Self {
315 type_,
316 items,
317 source: metadata
318 .source
319 .trim_remainder(next_item_source)
320 .trim_trailing_line_end()
321 .trim_trailing_whitespace(),
322 title_source: metadata.title_source,
323 title: metadata.title.clone(),
324 anchor: metadata.anchor,
325 anchor_reftext: metadata.anchor_reftext,
326 attrlist: metadata.attrlist.clone(),
327 is_checklist,
328 is_bibliography,
329 },
330 after: next_item_source,
331 })
332 }
333
334 pub fn type_(&self) -> ListType {
336 self.type_
337 }
338
339 pub fn is_checklist(&self) -> bool {
347 self.is_checklist
348 }
349
350 pub fn is_bibliography(&self) -> bool {
359 self.is_bibliography
360 }
361
362 pub fn marker_style(&self) -> Option<&'static str> {
370 let first_marker = self.items.first()?.as_list_item()?.list_item_marker();
371
372 match first_marker {
373 ListItemMarker::Dots(span) => {
374 let marker_len = span.data().len();
375 match marker_len {
376 1 => Some("arabic"),
377 2 => Some("loweralpha"),
378 3 => Some("lowerroman"),
379 4 => Some("upperalpha"),
380 5 => Some("upperroman"),
381 _ => Some("arabic"),
382 }
383 }
384 ListItemMarker::ArabicNumeral(_) => Some("arabic"),
385 ListItemMarker::Callout(_) => Some("arabic"),
386 ListItemMarker::AlphaListLower(_) => Some("loweralpha"),
387 ListItemMarker::AlphaListCapital(_) => Some("upperalpha"),
388 ListItemMarker::RomanNumeralLower(_) => Some("lowerroman"),
389 ListItemMarker::RomanNumeralUpper(_) => Some("upperroman"),
390 _ => None,
391 }
392 }
393
394 pub fn start(&self) -> Option<i64> {
412 if self.type_ != ListType::Ordered {
413 return None;
414 }
415
416 let resolved = self
419 .attrlist
420 .as_ref()
421 .and_then(|attrlist| attrlist.named_attribute("start"))
422 .and_then(|attr| attr.value().trim().parse::<i64>().ok())
423 .or_else(|| {
424 self.items
425 .first()
426 .and_then(|item| item.as_list_item())
427 .and_then(|li| li.list_item_marker().ordinal_value())
428 .map(i64::from)
429 });
430
431 resolved.filter(|&n| n != 1)
434 }
435}
436
437impl<'src> IsBlock<'src> for ListBlock<'src> {
438 fn content_model(&self) -> ContentModel {
439 ContentModel::Compound
440 }
441
442 fn raw_context(&self) -> CowStr<'src> {
443 "list".into()
444 }
445
446 fn child_blocks_mut(&mut self) -> &mut [Block<'src>] {
447 &mut self.items
448 }
449
450 fn title_source(&'src self) -> Option<Span<'src>> {
451 self.title_source
452 }
453
454 fn title(&self) -> Option<&str> {
455 self.title.as_ref().map(Content::rendered_str)
456 }
457
458 fn anchor(&'src self) -> Option<Span<'src>> {
459 self.anchor
460 }
461
462 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
463 self.anchor_reftext
464 }
465
466 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
467 self.attrlist.as_ref()
468 }
469
470 fn resolved_style(&'src self) -> Option<&'src str> {
471 self.declared_style()
479 .or_else(|| self.is_bibliography.then_some("bibliography"))
480 }
481}
482
483impl<'src> HasSpan<'src> for ListBlock<'src> {
484 fn span(&self) -> Span<'src> {
485 self.source
486 }
487}
488
489impl std::fmt::Debug for ListBlock<'_> {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 f.debug_struct("ListBlock")
492 .field("type_", &self.type_)
493 .field("items", &DebugSliceReference(&self.items))
494 .field("source", &self.source)
495 .field("title_source", &self.title_source)
496 .field("title", &self.title)
497 .field("anchor", &self.anchor)
498 .field("anchor_reftext", &self.anchor_reftext)
499 .field("attrlist", &self.attrlist)
500 .field("is_checklist", &self.is_checklist)
501 .field("is_bibliography", &self.is_bibliography)
502 .finish()
503 }
504}
505
506#[derive(Clone, Copy, Eq, Hash, PartialEq)]
508pub enum ListType {
509 Unordered,
512
513 Ordered,
516
517 Description,
520
521 Callout,
525}
526
527impl std::fmt::Debug for ListType {
528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 match self {
530 ListType::Unordered => write!(f, "ListType::Unordered"),
531 ListType::Ordered => write!(f, "ListType::Ordered"),
532 ListType::Description => write!(f, "ListType::Description"),
533 ListType::Callout => write!(f, "ListType::Callout"),
534 }
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 #![allow(clippy::indexing_slicing)]
541 #![allow(clippy::panic)]
542 #![allow(clippy::unwrap_used)]
543
544 use crate::{
545 blocks::{ContentModel, ListType, metadata::BlockMetadata},
546 span::MatchedItem,
547 tests::prelude::*,
548 warnings::Warning,
549 };
550
551 fn list_parse<'a>(source: &'a str) -> Option<MatchedItem<'a, crate::blocks::ListBlock<'a>>> {
552 let mut parser = crate::Parser::default();
553 let mut warnings: Vec<Warning<'a>> = vec![];
554
555 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
556
557 let result = crate::blocks::list::ListBlock::parse(&metadata, &mut parser, &mut warnings);
558
559 assert!(warnings.is_empty());
560
561 result
562 }
563
564 fn list_parse_with_warnings<'a>(
568 source: &'a str,
569 ) -> (
570 Option<MatchedItem<'a, crate::blocks::ListBlock<'a>>>,
571 Vec<Warning<'a>>,
572 ) {
573 let mut parser = crate::Parser::default();
574 let mut warnings: Vec<Warning<'a>> = vec![];
575
576 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
577
578 let result = crate::blocks::list::ListBlock::parse(&metadata, &mut parser, &mut warnings);
579
580 (result, warnings)
581 }
582
583 #[test]
584 fn basic_case() {
585 assert!(list_parse("-xyz").is_none());
586 assert!(list_parse("-- x").is_none());
587
588 let list = list_parse("- blah").unwrap();
589
590 assert_eq!(
591 list.item,
592 ListBlock {
593 type_: ListType::Unordered,
594 items: &[Block::ListItem(ListItem {
595 marker: ListItemMarker::Hyphen(Span {
596 data: "-",
597 line: 1,
598 col: 1,
599 offset: 0,
600 },),
601 blocks: &[Block::Simple(SimpleBlock {
602 content: Content {
603 original: Span {
604 data: "blah",
605 line: 1,
606 col: 3,
607 offset: 2,
608 },
609 rendered: "blah",
610 },
611 source: Span {
612 data: "blah",
613 line: 1,
614 col: 3,
615 offset: 2,
616 },
617 style: SimpleBlockStyle::Paragraph,
618 title_source: None,
619 title: None,
620 caption: None,
621 number: None,
622 anchor: None,
623 anchor_reftext: None,
624 attrlist: None,
625 },),],
626 source: Span {
627 data: "- blah",
628 line: 1,
629 col: 1,
630 offset: 0,
631 },
632 anchor: None,
633 anchor_reftext: None,
634 attrlist: None,
635 },),],
636 source: Span {
637 data: "- blah",
638 line: 1,
639 col: 1,
640 offset: 0,
641 },
642 title_source: None,
643 title: None,
644 anchor: None,
645 anchor_reftext: None,
646 attrlist: None,
647 }
648 );
649
650 assert_eq!(list.item.type_(), ListType::Unordered);
651 assert_eq!(list.item.content_model(), ContentModel::Compound);
652 assert_eq!(list.item.raw_context().as_ref(), "list");
653
654 let mut list_blocks = list.item.child_blocks();
655
656 let list_item = list_blocks.next().unwrap();
657
658 assert_eq!(
659 list_item,
660 &Block::ListItem(ListItem {
661 marker: ListItemMarker::Hyphen(Span {
662 data: "-",
663 line: 1,
664 col: 1,
665 offset: 0,
666 },),
667 blocks: &[Block::Simple(SimpleBlock {
668 content: Content {
669 original: Span {
670 data: "blah",
671 line: 1,
672 col: 3,
673 offset: 2,
674 },
675 rendered: "blah",
676 },
677 source: Span {
678 data: "blah",
679 line: 1,
680 col: 3,
681 offset: 2,
682 },
683 style: SimpleBlockStyle::Paragraph,
684 title_source: None,
685 title: None,
686 caption: None,
687 number: None,
688 anchor: None,
689 anchor_reftext: None,
690 attrlist: None,
691 },),],
692 source: Span {
693 data: "- blah",
694 line: 1,
695 col: 1,
696 offset: 0,
697 },
698 anchor: None,
699 anchor_reftext: None,
700 attrlist: None,
701 })
702 );
703
704 assert_eq!(list_item.content_model(), ContentModel::Compound);
705 assert_eq!(list_item.raw_context().as_ref(), "list_item");
706
707 let mut li_blocks = list_item.child_blocks();
708
709 assert_eq!(
710 li_blocks.next().unwrap(),
711 &Block::Simple(SimpleBlock {
712 content: Content {
713 original: Span {
714 data: "blah",
715 line: 1,
716 col: 3,
717 offset: 2,
718 },
719 rendered: "blah",
720 },
721 source: Span {
722 data: "blah",
723 line: 1,
724 col: 3,
725 offset: 2,
726 },
727 style: SimpleBlockStyle::Paragraph,
728 title_source: None,
729 title: None,
730 caption: None,
731 number: None,
732 anchor: None,
733 anchor_reftext: None,
734 attrlist: None,
735 })
736 );
737 assert!(li_blocks.next().is_none());
738
739 assert!(list_item.title_source().is_none());
740 assert!(list_item.title().is_none());
741 assert!(list_item.anchor().is_none());
742 assert!(list_item.anchor_reftext().is_none());
743 assert!(list_item.attrlist().is_none());
744 assert_eq!(list_item.substitution_group(), SubstitutionGroup::Normal);
745 assert_eq!(
746 list_item.span(),
747 Span {
748 data: "- blah",
749 line: 1,
750 col: 1,
751 offset: 0,
752 }
753 );
754
755 assert!(list_blocks.next().is_none());
756
757 assert!(list.item.title_source().is_none());
758 assert!(list.item.title().is_none());
759 assert!(list.item.anchor().is_none());
760 assert!(list.item.anchor_reftext().is_none());
761 assert!(list.item.attrlist().is_none());
762
763 assert_eq!(
764 format!("{:#?}", list.item),
765 "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 has_empty_principal_text: false,\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}"
766 );
767
768 assert_eq!(
769 list.after,
770 Span {
771 data: "",
772 line: 1,
773 col: 7,
774 offset: 6,
775 }
776 );
777 }
778
779 #[test]
780 fn list_type_impl_debug() {
781 assert_eq!(format!("{:#?}", ListType::Unordered), "ListType::Unordered");
782 assert_eq!(format!("{:#?}", ListType::Ordered), "ListType::Ordered");
783
784 assert_eq!(
785 format!("{:#?}", ListType::Description),
786 "ListType::Description"
787 );
788
789 assert_eq!(format!("{:#?}", ListType::Callout), "ListType::Callout");
790 }
791
792 #[test]
793 fn callout_list() {
794 let (list, warnings) = list_parse_with_warnings("<1> First\n<2> Second\n");
797 let list = list.unwrap();
798
799 assert_eq!(list.item.type_(), ListType::Callout);
800 assert_eq!(list.item.marker_style(), Some("arabic"));
801
802 let items: Vec<_> = list.item.child_blocks().collect();
803 assert_eq!(items.len(), 2);
804
805 assert_eq!(
806 items[0].child_blocks().next().unwrap().rendered_content(),
807 Some("First")
808 );
809 assert_eq!(
810 items[1].child_blocks().next().unwrap().rendered_content(),
811 Some("Second")
812 );
813
814 let warning_types: Vec<_> = warnings.iter().map(|w| &w.warning).collect();
815 assert_eq!(
816 warning_types,
817 vec![
818 &WarningType::NoCalloutFound(1),
819 &WarningType::NoCalloutFound(2),
820 ]
821 );
822 }
823
824 #[test]
825 fn callout_list_auto_numbered() {
826 let (list, warnings) = list_parse_with_warnings("<.> First\n<.> Second\n<.> Third\n");
828 let list = list.unwrap();
829
830 assert_eq!(list.item.type_(), ListType::Callout);
831 assert_eq!(list.item.child_blocks().count(), 3);
832
833 assert_eq!(warnings.len(), 3);
835 }
836
837 #[test]
838 fn callout_list_marker_only_trailing_bracket_is_not_a_list() {
839 assert!(list_parse("1> Not a callout list item\n").is_none());
841 }
842
843 #[test]
844 fn attrlist_doesnt_exit() {
845 let list = list_parse("* Foo\n[loweralpha]\n. Boo\n* Blech").unwrap();
846
847 assert_eq!(
848 list.item,
849 ListBlock {
850 type_: ListType::Unordered,
851 items: &[
852 Block::ListItem(ListItem {
853 marker: ListItemMarker::Asterisks(Span {
854 data: "*",
855 line: 1,
856 col: 1,
857 offset: 0,
858 },),
859 blocks: &[
860 Block::Simple(SimpleBlock {
861 content: Content {
862 original: Span {
863 data: "Foo",
864 line: 1,
865 col: 3,
866 offset: 2,
867 },
868 rendered: "Foo",
869 },
870 source: Span {
871 data: "Foo",
872 line: 1,
873 col: 3,
874 offset: 2,
875 },
876 style: SimpleBlockStyle::Paragraph,
877 title_source: None,
878 title: None,
879 caption: None,
880 number: None,
881 anchor: None,
882 anchor_reftext: None,
883 attrlist: None,
884 },),
885 Block::List(ListBlock {
886 type_: ListType::Ordered,
887 items: &[Block::ListItem(ListItem {
888 marker: ListItemMarker::Dots(Span {
889 data: ".",
890 line: 3,
891 col: 1,
892 offset: 19,
893 },),
894 blocks: &[Block::Simple(SimpleBlock {
895 content: Content {
896 original: Span {
897 data: "Boo",
898 line: 3,
899 col: 3,
900 offset: 21,
901 },
902 rendered: "Boo",
903 },
904 source: Span {
905 data: "Boo",
906 line: 3,
907 col: 3,
908 offset: 21,
909 },
910 style: SimpleBlockStyle::Paragraph,
911 title_source: None,
912 title: None,
913 caption: None,
914 number: None,
915 anchor: None,
916 anchor_reftext: None,
917 attrlist: None,
918 },),],
919 source: Span {
920 data: ". Boo",
921 line: 3,
922 col: 1,
923 offset: 19,
924 },
925 anchor: None,
926 anchor_reftext: None,
927 attrlist: None,
928 },),],
929 source: Span {
930 data: "[loweralpha]\n. Boo",
931 line: 2,
932 col: 1,
933 offset: 6,
934 },
935 title_source: None,
936 title: None,
937 anchor: None,
938 anchor_reftext: None,
939 attrlist: Some(Attrlist {
940 attributes: &[ElementAttribute {
941 name: None,
942 value: "loweralpha",
943 shorthand_items: &["loweralpha"],
944 },],
945 anchor: None,
946 source: Span {
947 data: "loweralpha",
948 line: 2,
949 col: 2,
950 offset: 7,
951 },
952 },),
953 },),
954 ],
955 source: Span {
956 data: "* Foo\n[loweralpha]\n. Boo",
957 line: 1,
958 col: 1,
959 offset: 0,
960 },
961 anchor: None,
962 anchor_reftext: None,
963 attrlist: None,
964 },),
965 Block::ListItem(ListItem {
966 marker: ListItemMarker::Asterisks(Span {
967 data: "*",
968 line: 4,
969 col: 1,
970 offset: 25,
971 },),
972 blocks: &[Block::Simple(SimpleBlock {
973 content: Content {
974 original: Span {
975 data: "Blech",
976 line: 4,
977 col: 3,
978 offset: 27,
979 },
980 rendered: "Blech",
981 },
982 source: Span {
983 data: "Blech",
984 line: 4,
985 col: 3,
986 offset: 27,
987 },
988 style: SimpleBlockStyle::Paragraph,
989 title_source: None,
990 title: None,
991 caption: None,
992 number: None,
993 anchor: None,
994 anchor_reftext: None,
995 attrlist: None,
996 },),],
997 source: Span {
998 data: "* Blech",
999 line: 4,
1000 col: 1,
1001 offset: 25,
1002 },
1003 anchor: None,
1004 anchor_reftext: None,
1005 attrlist: None,
1006 },),
1007 ],
1008 source: Span {
1009 data: "* Foo\n[loweralpha]\n. Boo\n* Blech",
1010 line: 1,
1011 col: 1,
1012 offset: 0,
1013 },
1014 title_source: None,
1015 title: None,
1016 anchor: None,
1017 anchor_reftext: None,
1018 attrlist: None,
1019 }
1020 );
1021
1022 assert_eq!(
1023 list.after,
1024 Span {
1025 data: "",
1026 line: 4,
1027 col: 8,
1028 offset: 32,
1029 }
1030 );
1031 }
1032
1033 #[test]
1034 fn metadata_merged_across_empty_lines_for_nested_list() {
1035 let list = list_parse("* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo\n* Blech").unwrap();
1038
1039 assert_eq!(
1040 list.item,
1041 ListBlock {
1042 type_: ListType::Unordered,
1043 items: &[
1044 Block::ListItem(ListItem {
1045 marker: ListItemMarker::Asterisks(Span {
1046 data: "*",
1047 line: 1,
1048 col: 1,
1049 offset: 0,
1050 },),
1051 blocks: &[
1052 Block::Simple(SimpleBlock {
1053 content: Content {
1054 original: Span {
1055 data: "Foo",
1056 line: 1,
1057 col: 3,
1058 offset: 2,
1059 },
1060 rendered: "Foo",
1061 },
1062 source: Span {
1063 data: "Foo",
1064 line: 1,
1065 col: 3,
1066 offset: 2,
1067 },
1068 style: SimpleBlockStyle::Paragraph,
1069 title_source: None,
1070 title: None,
1071 caption: None,
1072 number: None,
1073 anchor: None,
1074 anchor_reftext: None,
1075 attrlist: None,
1076 },),
1077 Block::List(ListBlock {
1078 type_: ListType::Ordered,
1079 items: &[Block::ListItem(ListItem {
1080 marker: ListItemMarker::Dots(Span {
1081 data: ".",
1082 line: 5,
1083 col: 1,
1084 offset: 31,
1085 },),
1086 blocks: &[Block::Simple(SimpleBlock {
1087 content: Content {
1088 original: Span {
1089 data: "Boo",
1090 line: 5,
1091 col: 3,
1092 offset: 33,
1093 },
1094 rendered: "Boo",
1095 },
1096 source: Span {
1097 data: "Boo",
1098 line: 5,
1099 col: 3,
1100 offset: 33,
1101 },
1102 style: SimpleBlockStyle::Paragraph,
1103 title_source: None,
1104 title: None,
1105 caption: None,
1106 number: None,
1107 anchor: None,
1108 anchor_reftext: None,
1109 attrlist: None,
1110 },),],
1111 source: Span {
1112 data: ". Boo",
1113 line: 5,
1114 col: 1,
1115 offset: 31,
1116 },
1117 anchor: None,
1118 anchor_reftext: None,
1119 attrlist: None,
1120 },),],
1121 source: Span {
1122 data: "[loweralpha]\n\n[[anchor]]\n. Boo",
1123 line: 2,
1124 col: 1,
1125 offset: 6,
1126 },
1127 title_source: None,
1128 title: None,
1129 anchor: Some(Span {
1130 data: "anchor",
1131 line: 4,
1132 col: 3,
1133 offset: 22,
1134 },),
1135 anchor_reftext: None,
1136 attrlist: Some(Attrlist {
1137 attributes: &[ElementAttribute {
1138 name: None,
1139 value: "loweralpha",
1140 shorthand_items: &["loweralpha"],
1141 },],
1142 anchor: None,
1143 source: Span {
1144 data: "loweralpha",
1145 line: 2,
1146 col: 2,
1147 offset: 7,
1148 },
1149 },),
1150 },),
1151 ],
1152 source: Span {
1153 data: "* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo",
1154 line: 1,
1155 col: 1,
1156 offset: 0,
1157 },
1158 anchor: None,
1159 anchor_reftext: None,
1160 attrlist: None,
1161 },),
1162 Block::ListItem(ListItem {
1163 marker: ListItemMarker::Asterisks(Span {
1164 data: "*",
1165 line: 6,
1166 col: 1,
1167 offset: 37,
1168 },),
1169 blocks: &[Block::Simple(SimpleBlock {
1170 content: Content {
1171 original: Span {
1172 data: "Blech",
1173 line: 6,
1174 col: 3,
1175 offset: 39,
1176 },
1177 rendered: "Blech",
1178 },
1179 source: Span {
1180 data: "Blech",
1181 line: 6,
1182 col: 3,
1183 offset: 39,
1184 },
1185 style: SimpleBlockStyle::Paragraph,
1186 title_source: None,
1187 title: None,
1188 caption: None,
1189 number: None,
1190 anchor: None,
1191 anchor_reftext: None,
1192 attrlist: None,
1193 },),],
1194 source: Span {
1195 data: "* Blech",
1196 line: 6,
1197 col: 1,
1198 offset: 37,
1199 },
1200 anchor: None,
1201 anchor_reftext: None,
1202 attrlist: None,
1203 },),
1204 ],
1205 source: Span {
1206 data: "* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo\n* Blech",
1207 line: 1,
1208 col: 1,
1209 offset: 0,
1210 },
1211 title_source: None,
1212 title: None,
1213 anchor: None,
1214 anchor_reftext: None,
1215 attrlist: None,
1216 }
1217 );
1218 }
1219
1220 #[test]
1221 fn parent_marker_after_metadata_separated_by_empty_lines() {
1222 let list =
1229 list_parse("* grandparent\n** parent\n*** nested\n[[anchor]]\n\n* back to grandparent")
1230 .unwrap();
1231
1232 assert_eq!(list.item.child_blocks().count(), 2);
1234 assert_eq!(list.item.type_(), ListType::Unordered);
1235
1236 let mut outer_items = list.item.child_blocks();
1237
1238 let first_outer = outer_items.next().unwrap();
1240 let first_outer_blocks: Vec<_> = first_outer.child_blocks().collect();
1241 assert_eq!(first_outer_blocks.len(), 2); let nested_list = &first_outer_blocks[1];
1245 assert_eq!(nested_list.child_blocks().count(), 1);
1246
1247 let parent_item = nested_list.child_blocks().next().unwrap();
1249 let parent_blocks: Vec<_> = parent_item.child_blocks().collect();
1250 assert_eq!(parent_blocks.len(), 2); let innermost_list = &parent_blocks[1];
1254 assert_eq!(innermost_list.child_blocks().count(), 1);
1255
1256 let innermost_item = innermost_list.child_blocks().next().unwrap();
1258 assert_eq!(innermost_item.child_blocks().count(), 1);
1259
1260 let second_outer = outer_items.next().unwrap();
1262 assert_eq!(second_outer.child_blocks().count(), 1);
1263 assert!(outer_items.next().is_none());
1264 }
1265
1266 #[test]
1267 fn block_metadata_on_a_subsequent_item_is_captured() {
1268 let list = list_parse("* one\n[[second]]\n[.special]\n* two").unwrap();
1272
1273 let items: Vec<_> = list.item.child_blocks().collect();
1274 assert_eq!(items.len(), 2);
1275
1276 assert_eq!(items[1].anchor().unwrap().data(), "second");
1278 assert_eq!(items[1].attrlist().unwrap().roles(), vec!["special"]);
1279
1280 assert!(items[0].anchor().is_none());
1282 assert!(items[0].attrlist().is_none());
1283 }
1284
1285 #[test]
1286 fn blank_line_before_metadata_starts_a_new_list() {
1287 let doc = crate::Parser::default().parse("* one\n\n[[second]]\n* two");
1291
1292 let lists: Vec<_> = doc
1293 .child_blocks()
1294 .filter(|b| b.raw_context().as_ref() == "list")
1295 .collect();
1296 assert_eq!(lists.len(), 2);
1297
1298 assert_eq!(lists[0].child_blocks().count(), 1);
1301 assert_eq!(lists[1].child_blocks().count(), 1);
1302 assert_eq!(lists[1].anchor().unwrap().data(), "second");
1303 }
1304
1305 #[test]
1306 fn marker_style_single_dot() {
1307 let list = list_parse(". Item one\n. Item two\n").unwrap();
1308 assert_eq!(list.item.marker_style(), Some("arabic"));
1309 }
1310
1311 #[test]
1312 fn marker_style_double_dots() {
1313 let list = list_parse(".. Item a\n.. Item b\n").unwrap();
1314 assert_eq!(list.item.marker_style(), Some("loweralpha"));
1315 }
1316
1317 #[test]
1318 fn marker_style_triple_dots() {
1319 let list = list_parse("... Item i\n... Item ii\n").unwrap();
1320 assert_eq!(list.item.marker_style(), Some("lowerroman"));
1321 }
1322
1323 #[test]
1324 fn marker_style_four_dots() {
1325 let list = list_parse(".... Item A\n.... Item B\n").unwrap();
1326 assert_eq!(list.item.marker_style(), Some("upperalpha"));
1327 }
1328
1329 #[test]
1330 fn marker_style_five_dots() {
1331 let list = list_parse("..... Item I\n..... Item II\n").unwrap();
1332 assert_eq!(list.item.marker_style(), Some("upperroman"));
1333 }
1334
1335 #[test]
1336 fn marker_style_hyphen_returns_none() {
1337 let list = list_parse("- Item one\n- Item two\n").unwrap();
1338 assert_eq!(list.item.marker_style(), None);
1339 }
1340
1341 #[test]
1342 fn marker_style_asterisk_returns_none() {
1343 let list = list_parse("* Item one\n* Item two\n").unwrap();
1344 assert_eq!(list.item.marker_style(), None);
1345 }
1346
1347 mod resolved_style {
1348 use super::list_parse;
1349 use crate::{
1350 Parser,
1351 blocks::{Block, FindBlocks, IsBlock},
1352 };
1353
1354 fn with_first_section_list(src: &str, check: impl FnOnce(&crate::blocks::ListBlock<'_>)) {
1358 let doc = Parser::default().parse(src);
1359
1360 let Some(Block::Section(section)) = doc.child_blocks().next() else {
1361 panic!("expected a section");
1362 };
1363
1364 let Some(Block::List(list)) = section.child_blocks().next() else {
1365 panic!("expected a list");
1366 };
1367
1368 check(list);
1369 }
1370
1371 #[test]
1372 fn none_for_a_plain_list() {
1373 let list = list_parse("* one\n* two").unwrap();
1374 assert_eq!(list.item.resolved_style(), None);
1375 }
1376
1377 #[test]
1378 fn reflects_an_explicit_declared_style() {
1379 let list = list_parse("[loweralpha]\n. one\n. two").unwrap();
1380 assert_eq!(list.item.declared_style(), Some("loweralpha"));
1381 assert_eq!(list.item.resolved_style(), Some("loweralpha"));
1382 }
1383
1384 #[test]
1385 fn explicit_bibliography_style() {
1386 let list = list_parse("[bibliography]\n* [[[a]]] An entry.").unwrap();
1387 assert!(list.item.is_bibliography());
1388 assert_eq!(list.item.declared_style(), Some("bibliography"));
1389 assert_eq!(list.item.resolved_style(), Some("bibliography"));
1390 }
1391
1392 #[test]
1393 fn inherited_from_a_bibliography_section() {
1394 with_first_section_list(
1397 "[bibliography]\n== References\n\n* [[[a]]] An entry.\n",
1398 |list| {
1399 assert!(list.is_bibliography());
1400 assert_eq!(list.declared_style(), None);
1401 assert_eq!(list.resolved_style(), Some("bibliography"));
1402 },
1403 );
1404 }
1405
1406 #[test]
1407 fn a_declared_style_suppresses_the_inherited_bibliography() {
1408 with_first_section_list(
1412 "[bibliography]\n== References\n\n[square]\n* An entry.\n",
1413 |list| {
1414 assert!(!list.is_bibliography());
1415 assert_eq!(list.declared_style(), Some("square"));
1416 assert_eq!(list.resolved_style(), Some("square"));
1417 },
1418 );
1419 }
1420 }
1421
1422 #[test]
1423 fn marker_with_no_content() {
1424 assert!(list_parse("- ").is_none());
1428 assert!(list_parse("* ").is_none());
1429 assert!(list_parse(". ").is_none());
1430 }
1431
1432 #[test]
1433 fn orphaned_title_after_continuation_is_discarded() {
1434 let list = list_parse("* item one\n+\n.Title\n\nsecond paragraph").unwrap();
1441
1442 let mut items = list.item.child_blocks();
1444 let item = items.next().unwrap();
1445 assert!(items.next().is_none());
1446
1447 let blocks: Vec<_> = item.child_blocks().collect();
1450 assert_eq!(blocks.len(), 2);
1451
1452 assert_eq!(
1454 blocks[0],
1455 &Block::Simple(SimpleBlock {
1456 content: Content {
1457 original: Span {
1458 data: "item one",
1459 line: 1,
1460 col: 3,
1461 offset: 2,
1462 },
1463 rendered: "item one",
1464 },
1465 source: Span {
1466 data: "item one",
1467 line: 1,
1468 col: 3,
1469 offset: 2,
1470 },
1471 style: SimpleBlockStyle::Paragraph,
1472 title_source: None,
1473 title: None,
1474 caption: None,
1475 number: None,
1476 anchor: None,
1477 anchor_reftext: None,
1478 attrlist: None,
1479 })
1480 );
1481
1482 assert_eq!(
1484 blocks[1],
1485 &Block::Simple(SimpleBlock {
1486 content: Content {
1487 original: Span {
1488 data: "second paragraph",
1489 line: 5,
1490 col: 1,
1491 offset: 21,
1492 },
1493 rendered: "second paragraph",
1494 },
1495 source: Span {
1496 data: "second paragraph",
1497 line: 5,
1498 col: 1,
1499 offset: 21,
1500 },
1501 style: SimpleBlockStyle::Paragraph,
1502 title_source: None,
1503 title: None,
1504 caption: None,
1505 number: None,
1506 anchor: None,
1507 anchor_reftext: None,
1508 attrlist: None,
1509 })
1510 );
1511 }
1512
1513 #[test]
1514 fn block_list_enum_case() {
1515 let mut parser = crate::Parser::default();
1516
1517 let mi = crate::blocks::Block::parse(crate::Span::new("- blah"), &mut parser)
1518 .unwrap_if_no_warnings()
1519 .unwrap();
1520
1521 assert!(matches!(mi.item, crate::blocks::Block::List(_)));
1522
1523 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1524 assert!(mi.item.rendered_content().is_none());
1525 assert_eq!(mi.item.raw_context().as_ref(), "list");
1526 assert_eq!(mi.item.child_blocks().count(), 1);
1527 assert!(mi.item.title_source().is_none());
1528 assert!(mi.item.title().is_none());
1529 assert!(mi.item.anchor().is_none());
1530 assert!(mi.item.anchor_reftext().is_none());
1531 assert!(mi.item.attrlist().is_none());
1532 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1533
1534 assert_eq!(
1535 mi.item.span(),
1536 Span {
1537 data: "- blah",
1538 line: 1,
1539 col: 1,
1540 offset: 0,
1541 }
1542 );
1543
1544 let debug_str = format!("{:?}", mi.item);
1545 assert!(debug_str.starts_with("Block::List("));
1546 }
1547
1548 mod start {
1549 use super::list_parse;
1550 use crate::blocks::ListType;
1551
1552 #[test]
1553 fn unordered_list_has_no_start() {
1554 let mi = list_parse("* one\n* two").unwrap();
1555 assert_eq!(mi.item.type_(), ListType::Unordered);
1556 assert_eq!(mi.item.start(), None);
1557 }
1558
1559 #[test]
1560 fn implicit_ordered_marker_has_no_start() {
1561 let mi = list_parse(". one\n. two").unwrap();
1564 assert_eq!(mi.item.type_(), ListType::Ordered);
1565 assert_eq!(mi.item.start(), None);
1566 }
1567
1568 #[test]
1569 fn explicit_arabic_first_marker_sets_start() {
1570 let mi = list_parse("7. one\n8. two").unwrap();
1571 assert_eq!(mi.item.type_(), ListType::Ordered);
1572 assert_eq!(mi.item.start(), Some(7));
1573 }
1574
1575 #[test]
1576 fn explicit_alpha_first_marker_sets_start() {
1577 let mi = list_parse("c. one\nd. two").unwrap();
1579 assert_eq!(mi.item.start(), Some(3));
1580 }
1581
1582 #[test]
1583 fn explicit_ordinal_one_marker_defaults_to_none() {
1584 assert_eq!(list_parse("1. one\n2. two").unwrap().item.start(), None);
1588 assert_eq!(list_parse("a. one\nb. two").unwrap().item.start(), None);
1589 }
1590
1591 #[test]
1592 fn start_attribute_takes_precedence() {
1593 let mi = list_parse("[start=5]\n. one\n. two").unwrap();
1594 assert_eq!(mi.item.type_(), ListType::Ordered);
1595 assert_eq!(mi.item.start(), Some(5));
1596 }
1597
1598 #[test]
1599 fn start_attribute_of_one_is_none() {
1600 let mi = list_parse("[start=1]\n. one\n. two").unwrap();
1602 assert_eq!(mi.item.start(), None);
1603 }
1604
1605 #[test]
1606 fn start_attribute_overrides_first_marker() {
1607 let mi = list_parse("[start=5]\n7. one\n8. two").unwrap();
1608 assert_eq!(mi.item.start(), Some(5));
1609 }
1610
1611 #[test]
1612 fn non_numeric_start_attribute_falls_back_to_marker() {
1613 let mi = list_parse("[start=abc]\n7. one\n8. two").unwrap();
1614 assert_eq!(mi.item.start(), Some(7));
1615 }
1616 }
1617
1618 mod has_empty_principal_text {
1619 use crate::blocks::{Block, FindBlocks};
1620
1621 fn items<'a>(doc: &'a crate::Document<'a>) -> Vec<&'a crate::blocks::ListItem<'a>> {
1623 let Some(Block::List(list)) = doc.child_blocks().next() else {
1624 panic!("expected a list block");
1625 };
1626
1627 list.child_blocks()
1628 .map(|item| item.as_list_item().unwrap())
1629 .collect()
1630 }
1631
1632 #[test]
1633 fn empty_principal_with_continuation_keeps_attached_block() {
1634 let doc = crate::Parser::default().parse(". {empty}\n+\n----\nprint(\"one\")\n----\n");
1640 let items = items(&doc);
1641
1642 assert_eq!(items.len(), 1);
1643 assert!(items[0].has_empty_principal_text());
1644
1645 let mut children = items[0].child_blocks();
1646 assert!(matches!(children.next(), Some(Block::RawDelimited(_))));
1647 assert!(children.next().is_none());
1648 }
1649
1650 #[test]
1651 fn bare_empty_principal_has_no_child_blocks() {
1652 let doc = crate::Parser::default().parse(". {empty}\n. second\n");
1655 let items = items(&doc);
1656
1657 assert_eq!(items.len(), 2);
1658 assert!(items[0].has_empty_principal_text());
1659 assert_eq!(items[0].child_blocks().count(), 0);
1660
1661 assert!(!items[1].has_empty_principal_text());
1663 }
1664
1665 #[test]
1666 fn non_empty_principal_with_continuation_is_not_flagged() {
1667 let doc = crate::Parser::default().parse(". text\n+\n----\nprint(\"one\")\n----\n");
1670 let items = items(&doc);
1671
1672 assert_eq!(items.len(), 1);
1673 assert!(!items[0].has_empty_principal_text());
1674
1675 let mut children = items[0].child_blocks();
1676 assert!(matches!(children.next(), Some(Block::Simple(_))));
1677 assert!(matches!(children.next(), Some(Block::RawDelimited(_))));
1678 assert!(children.next().is_none());
1679 }
1680 }
1681}