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