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 first_marker: ListItemMarker<'src>,
23 source: Span<'src>,
24 title_source: Option<Span<'src>>,
25 title: Option<String>,
26 anchor: Option<Span<'src>>,
27 anchor_reftext: Option<Span<'src>>,
28 attrlist: Option<Attrlist<'src>>,
29}
30
31impl<'src> ListBlock<'src> {
32 pub(crate) fn parse(
33 metadata: &BlockMetadata<'src>,
34 parser: &mut Parser,
35 warnings: &mut Vec<Warning<'src>>,
36 ) -> Option<MatchedItem<'src, Self>> {
37 Self::parse_inside_list(metadata, &[], parser, warnings)
38 }
39
40 pub(crate) fn parse_inside_list(
41 metadata: &BlockMetadata<'src>,
42 parent_list_markers: &[ListItemMarker<'src>],
43 parser: &mut Parser,
44 warnings: &mut Vec<Warning<'src>>,
45 ) -> Option<MatchedItem<'src, Self>> {
46 let source = metadata.block_start.discard_empty_lines();
47
48 let mut items: Vec<Block<'src>> = vec![];
49 let mut next_item_source = source;
50 let mut first_marker: Option<ListItemMarker<'src>> = None;
51 let mut expected_ordinal: Option<u32> = None;
52
53 loop {
54 let next_line_mi = next_item_source.take_normalized_line();
55
56 if next_line_mi.item.data().is_empty() || next_line_mi.item.data() == "+" {
57 if next_item_source.is_empty() || !parent_list_markers.is_empty() {
58 break;
59 } else {
60 next_item_source = next_line_mi.after;
61 continue;
62 }
63 }
64
65 let list_item_metadata = BlockMetadata {
67 title_source: None,
68 title: None,
69 anchor: None,
70 anchor_reftext: None,
71 attrlist: None,
72 source: next_item_source,
73 block_start: next_item_source,
74 };
75
76 let Some(list_item_marker_mi) =
77 ListItemMarker::parse(list_item_metadata.block_start, parser)
78 else {
79 break;
80 };
81
82 let this_item_marker = list_item_marker_mi.item;
83
84 if let Some(ref first_marker) = first_marker {
87 if !first_marker.is_match_for(&this_item_marker)
88 && parent_list_markers
89 .iter()
90 .any(|parent| parent.is_match_for(&this_item_marker))
91 {
92 break;
95 }
96
97 if let Some(actual_ordinal) = this_item_marker.ordinal_value() {
99 if let Some(expected) = expected_ordinal
100 && actual_ordinal != expected
101 {
102 if let (Some(expected_text), Some(actual_text)) = (
104 first_marker.ordinal_to_marker_text(expected),
105 first_marker.ordinal_to_marker_text(actual_ordinal),
106 ) {
107 warnings.push(Warning {
108 source: this_item_marker.span(),
109 warning: WarningType::ListItemOutOfSequence(
110 expected_text,
111 actual_text,
112 ),
113 });
114 }
115 }
116 expected_ordinal = Some(actual_ordinal + 1);
117 }
118 } else {
119 first_marker = Some(this_item_marker.clone());
120
121 if let Some(ordinal) = this_item_marker.ordinal_value() {
123 expected_ordinal = Some(ordinal + 1);
124 }
125 }
126
127 let Some(list_item_mi) =
128 ListItem::parse(&list_item_metadata, parent_list_markers, parser, warnings)
129 else {
130 break;
131 };
132
133 items.push(Block::ListItem(list_item_mi.item));
134 next_item_source = list_item_mi.after;
135 }
136
137 if items.is_empty() {
138 return None;
139 }
140
141 let first_marker = first_marker?;
142 let type_ = match first_marker {
143 ListItemMarker::Asterisks(_) => ListType::Unordered,
144 ListItemMarker::Hyphen(_) => ListType::Unordered,
145 ListItemMarker::Bullet(_) => ListType::Unordered,
146 ListItemMarker::Dots(_) => ListType::Ordered,
147 ListItemMarker::AlphaListCapital(_) => ListType::Ordered,
148 ListItemMarker::AlphaListLower(_) => ListType::Ordered,
149 ListItemMarker::RomanNumeralLower(_) => ListType::Ordered,
150 ListItemMarker::RomanNumeralUpper(_) => ListType::Ordered,
151 ListItemMarker::ArabicNumeral(_) => ListType::Ordered,
152
153 ListItemMarker::DefinedTerm {
154 term: _,
155 marker: _,
156 source: _,
157 } => ListType::Description,
158 };
159
160 Some(MatchedItem {
161 item: Self {
162 type_,
163 items,
164 first_marker,
165 source: metadata
166 .source
167 .trim_remainder(next_item_source)
168 .trim_trailing_line_end()
169 .trim_trailing_whitespace(),
170 title_source: metadata.title_source,
171 title: metadata.title.clone(),
172 anchor: metadata.anchor,
173 anchor_reftext: metadata.anchor_reftext,
174 attrlist: metadata.attrlist.clone(),
175 },
176 after: next_item_source,
177 })
178 }
179
180 pub fn type_(&self) -> ListType {
182 self.type_
183 }
184
185 pub fn marker_style(&self) -> Option<&'static str> {
193 match &self.first_marker {
194 ListItemMarker::Dots(span) => {
195 let marker_len = span.data().len();
196 match marker_len {
197 1 => Some("arabic"),
198 2 => Some("loweralpha"),
199 3 => Some("lowerroman"),
200 4 => Some("upperalpha"),
201 5 => Some("upperroman"),
202 _ => Some("arabic"),
203 }
204 }
205 ListItemMarker::ArabicNumeral(_) => Some("arabic"),
206 ListItemMarker::AlphaListLower(_) => Some("loweralpha"),
207 ListItemMarker::AlphaListCapital(_) => Some("upperalpha"),
208 ListItemMarker::RomanNumeralLower(_) => Some("lowerroman"),
209 ListItemMarker::RomanNumeralUpper(_) => Some("upperroman"),
210 _ => None,
211 }
212 }
213}
214
215impl<'src> IsBlock<'src> for ListBlock<'src> {
216 fn content_model(&self) -> ContentModel {
217 ContentModel::Compound
218 }
219
220 fn raw_context(&self) -> CowStr<'src> {
221 "list".into()
222 }
223
224 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
225 self.items.iter()
226 }
227
228 fn title_source(&'src self) -> Option<Span<'src>> {
229 self.title_source
230 }
231
232 fn title(&self) -> Option<&str> {
233 self.title.as_deref()
234 }
235
236 fn anchor(&'src self) -> Option<Span<'src>> {
237 self.anchor
238 }
239
240 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
241 self.anchor_reftext
242 }
243
244 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
245 self.attrlist.as_ref()
246 }
247}
248
249impl<'src> HasSpan<'src> for ListBlock<'src> {
250 fn span(&self) -> Span<'src> {
251 self.source
252 }
253}
254
255impl std::fmt::Debug for ListBlock<'_> {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 f.debug_struct("ListBlock")
258 .field("type_", &self.type_)
259 .field("items", &DebugSliceReference(&self.items))
260 .field("first_marker", &self.first_marker)
261 .field("source", &self.source)
262 .field("title_source", &self.title_source)
263 .field("title", &self.title)
264 .field("anchor", &self.anchor)
265 .field("anchor_reftext", &self.anchor_reftext)
266 .field("attrlist", &self.attrlist)
267 .finish()
268 }
269}
270
271#[derive(Clone, Copy, Eq, PartialEq)]
273pub enum ListType {
274 Unordered,
277
278 Ordered,
281
282 Description,
285}
286
287impl std::fmt::Debug for ListType {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 ListType::Unordered => write!(f, "ListType::Unordered"),
291 ListType::Ordered => write!(f, "ListType::Ordered"),
292 ListType::Description => write!(f, "ListType::Description"),
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 #![allow(clippy::indexing_slicing)]
300 #![allow(clippy::panic)]
301 #![allow(clippy::unwrap_used)]
302
303 use pretty_assertions_sorted::assert_eq;
304
305 use crate::{
306 HasSpan,
307 blocks::{ContentModel, IsBlock, ListType, SimpleBlockStyle, metadata::BlockMetadata},
308 content::SubstitutionGroup,
309 span::MatchedItem,
310 tests::prelude::*,
311 warnings::Warning,
312 };
313
314 fn list_parse<'a>(source: &'a str) -> Option<MatchedItem<'a, crate::blocks::ListBlock<'a>>> {
315 let mut parser = crate::Parser::default();
316 let mut warnings: Vec<Warning<'a>> = vec![];
317
318 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
319
320 let result = crate::blocks::list::ListBlock::parse(&metadata, &mut parser, &mut warnings);
321
322 assert!(warnings.is_empty());
323
324 result
325 }
326
327 #[test]
328 fn basic_case() {
329 assert!(list_parse("-xyz").is_none());
330 assert!(list_parse("-- x").is_none());
331
332 let list = list_parse("- blah").unwrap();
333
334 assert_eq!(
335 list.item,
336 ListBlock {
337 type_: ListType::Unordered,
338 items: &[Block::ListItem(ListItem {
339 marker: ListItemMarker::Hyphen(Span {
340 data: "-",
341 line: 1,
342 col: 1,
343 offset: 0,
344 },),
345 blocks: &[Block::Simple(SimpleBlock {
346 content: Content {
347 original: Span {
348 data: "blah",
349 line: 1,
350 col: 3,
351 offset: 2,
352 },
353 rendered: "blah",
354 },
355 source: Span {
356 data: "blah",
357 line: 1,
358 col: 3,
359 offset: 2,
360 },
361 style: SimpleBlockStyle::Paragraph,
362 title_source: None,
363 title: None,
364 anchor: None,
365 anchor_reftext: None,
366 attrlist: None,
367 },),],
368 source: Span {
369 data: "- blah",
370 line: 1,
371 col: 1,
372 offset: 0,
373 },
374 anchor: None,
375 anchor_reftext: None,
376 attrlist: None,
377 },),],
378 source: Span {
379 data: "- blah",
380 line: 1,
381 col: 1,
382 offset: 0,
383 },
384 title_source: None,
385 title: None,
386 anchor: None,
387 anchor_reftext: None,
388 attrlist: None,
389 }
390 );
391
392 assert_eq!(list.item.type_(), ListType::Unordered);
393 assert_eq!(list.item.content_model(), ContentModel::Compound);
394 assert_eq!(list.item.raw_context().as_ref(), "list");
395
396 let mut list_blocks = list.item.nested_blocks();
397
398 let list_item = list_blocks.next().unwrap();
399
400 assert_eq!(
401 list_item,
402 &Block::ListItem(ListItem {
403 marker: ListItemMarker::Hyphen(Span {
404 data: "-",
405 line: 1,
406 col: 1,
407 offset: 0,
408 },),
409 blocks: &[Block::Simple(SimpleBlock {
410 content: Content {
411 original: Span {
412 data: "blah",
413 line: 1,
414 col: 3,
415 offset: 2,
416 },
417 rendered: "blah",
418 },
419 source: Span {
420 data: "blah",
421 line: 1,
422 col: 3,
423 offset: 2,
424 },
425 style: SimpleBlockStyle::Paragraph,
426 title_source: None,
427 title: None,
428 anchor: None,
429 anchor_reftext: None,
430 attrlist: None,
431 },),],
432 source: Span {
433 data: "- blah",
434 line: 1,
435 col: 1,
436 offset: 0,
437 },
438 anchor: None,
439 anchor_reftext: None,
440 attrlist: None,
441 })
442 );
443
444 assert_eq!(list_item.content_model(), ContentModel::Compound);
445 assert_eq!(list_item.raw_context().as_ref(), "list_item");
446
447 let mut li_blocks = list_item.nested_blocks();
448
449 assert_eq!(
450 li_blocks.next().unwrap(),
451 &Block::Simple(SimpleBlock {
452 content: Content {
453 original: Span {
454 data: "blah",
455 line: 1,
456 col: 3,
457 offset: 2,
458 },
459 rendered: "blah",
460 },
461 source: Span {
462 data: "blah",
463 line: 1,
464 col: 3,
465 offset: 2,
466 },
467 style: SimpleBlockStyle::Paragraph,
468 title_source: None,
469 title: None,
470 anchor: None,
471 anchor_reftext: None,
472 attrlist: None,
473 })
474 );
475 assert!(li_blocks.next().is_none());
476
477 assert!(list_item.title_source().is_none());
478 assert!(list_item.title().is_none());
479 assert!(list_item.anchor().is_none());
480 assert!(list_item.anchor_reftext().is_none());
481 assert!(list_item.attrlist().is_none());
482 assert_eq!(list_item.substitution_group(), SubstitutionGroup::Normal);
483 assert_eq!(
484 list_item.span(),
485 Span {
486 data: "- blah",
487 line: 1,
488 col: 1,
489 offset: 0,
490 }
491 );
492
493 assert!(list_blocks.next().is_none());
494
495 assert!(list.item.title_source().is_none());
496 assert!(list.item.title().is_none());
497 assert!(list.item.anchor().is_none());
498 assert!(list.item.anchor_reftext().is_none());
499 assert!(list.item.attrlist().is_none());
500
501 assert_eq!(
502 format!("{:#?}", list.item),
503 "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 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 },\n ),\n ],\n first_marker: ListItemMarker::Hyphen(\n Span {\n data: \"-\",\n line: 1,\n col: 1,\n offset: 0,\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}"
504 );
505
506 assert_eq!(
507 list.after,
508 Span {
509 data: "",
510 line: 1,
511 col: 7,
512 offset: 6,
513 }
514 );
515 }
516
517 #[test]
518 fn list_type_impl_debug() {
519 assert_eq!(format!("{:#?}", ListType::Unordered), "ListType::Unordered");
520 assert_eq!(format!("{:#?}", ListType::Ordered), "ListType::Ordered");
521
522 assert_eq!(
523 format!("{:#?}", ListType::Description),
524 "ListType::Description"
525 );
526 }
527
528 #[test]
529 fn attrlist_doesnt_exit() {
530 let list = list_parse("* Foo\n[loweralpha]\n. Boo\n* Blech").unwrap();
531
532 assert_eq!(
533 list.item,
534 ListBlock {
535 type_: ListType::Unordered,
536 items: &[
537 Block::ListItem(ListItem {
538 marker: ListItemMarker::Asterisks(Span {
539 data: "*",
540 line: 1,
541 col: 1,
542 offset: 0,
543 },),
544 blocks: &[
545 Block::Simple(SimpleBlock {
546 content: Content {
547 original: Span {
548 data: "Foo",
549 line: 1,
550 col: 3,
551 offset: 2,
552 },
553 rendered: "Foo",
554 },
555 source: Span {
556 data: "Foo",
557 line: 1,
558 col: 3,
559 offset: 2,
560 },
561 style: SimpleBlockStyle::Paragraph,
562 title_source: None,
563 title: None,
564 anchor: None,
565 anchor_reftext: None,
566 attrlist: None,
567 },),
568 Block::List(ListBlock {
569 type_: ListType::Ordered,
570 items: &[Block::ListItem(ListItem {
571 marker: ListItemMarker::Dots(Span {
572 data: ".",
573 line: 3,
574 col: 1,
575 offset: 19,
576 },),
577 blocks: &[Block::Simple(SimpleBlock {
578 content: Content {
579 original: Span {
580 data: "Boo",
581 line: 3,
582 col: 3,
583 offset: 21,
584 },
585 rendered: "Boo",
586 },
587 source: Span {
588 data: "Boo",
589 line: 3,
590 col: 3,
591 offset: 21,
592 },
593 style: SimpleBlockStyle::Paragraph,
594 title_source: None,
595 title: None,
596 anchor: None,
597 anchor_reftext: None,
598 attrlist: None,
599 },),],
600 source: Span {
601 data: ". Boo",
602 line: 3,
603 col: 1,
604 offset: 19,
605 },
606 anchor: None,
607 anchor_reftext: None,
608 attrlist: None,
609 },),],
610 source: Span {
611 data: "[loweralpha]\n. Boo",
612 line: 2,
613 col: 1,
614 offset: 6,
615 },
616 title_source: None,
617 title: None,
618 anchor: None,
619 anchor_reftext: None,
620 attrlist: Some(Attrlist {
621 attributes: &[ElementAttribute {
622 name: None,
623 value: "loweralpha",
624 shorthand_items: &["loweralpha"],
625 },],
626 anchor: None,
627 source: Span {
628 data: "loweralpha",
629 line: 2,
630 col: 2,
631 offset: 7,
632 },
633 },),
634 },),
635 ],
636 source: Span {
637 data: "* Foo\n[loweralpha]\n. Boo",
638 line: 1,
639 col: 1,
640 offset: 0,
641 },
642 anchor: None,
643 anchor_reftext: None,
644 attrlist: None,
645 },),
646 Block::ListItem(ListItem {
647 marker: ListItemMarker::Asterisks(Span {
648 data: "*",
649 line: 4,
650 col: 1,
651 offset: 25,
652 },),
653 blocks: &[Block::Simple(SimpleBlock {
654 content: Content {
655 original: Span {
656 data: "Blech",
657 line: 4,
658 col: 3,
659 offset: 27,
660 },
661 rendered: "Blech",
662 },
663 source: Span {
664 data: "Blech",
665 line: 4,
666 col: 3,
667 offset: 27,
668 },
669 style: SimpleBlockStyle::Paragraph,
670 title_source: None,
671 title: None,
672 anchor: None,
673 anchor_reftext: None,
674 attrlist: None,
675 },),],
676 source: Span {
677 data: "* Blech",
678 line: 4,
679 col: 1,
680 offset: 25,
681 },
682 anchor: None,
683 anchor_reftext: None,
684 attrlist: None,
685 },),
686 ],
687 source: Span {
688 data: "* Foo\n[loweralpha]\n. Boo\n* Blech",
689 line: 1,
690 col: 1,
691 offset: 0,
692 },
693 title_source: None,
694 title: None,
695 anchor: None,
696 anchor_reftext: None,
697 attrlist: None,
698 }
699 );
700
701 assert_eq!(
702 list.after,
703 Span {
704 data: "",
705 line: 4,
706 col: 8,
707 offset: 32,
708 }
709 );
710 }
711
712 #[test]
713 fn metadata_merged_across_empty_lines_for_nested_list() {
714 let list = list_parse("* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo\n* Blech").unwrap();
717
718 assert_eq!(
719 list.item,
720 ListBlock {
721 type_: ListType::Unordered,
722 items: &[
723 Block::ListItem(ListItem {
724 marker: ListItemMarker::Asterisks(Span {
725 data: "*",
726 line: 1,
727 col: 1,
728 offset: 0,
729 },),
730 blocks: &[
731 Block::Simple(SimpleBlock {
732 content: Content {
733 original: Span {
734 data: "Foo",
735 line: 1,
736 col: 3,
737 offset: 2,
738 },
739 rendered: "Foo",
740 },
741 source: Span {
742 data: "Foo",
743 line: 1,
744 col: 3,
745 offset: 2,
746 },
747 style: SimpleBlockStyle::Paragraph,
748 title_source: None,
749 title: None,
750 anchor: None,
751 anchor_reftext: None,
752 attrlist: None,
753 },),
754 Block::List(ListBlock {
755 type_: ListType::Ordered,
756 items: &[Block::ListItem(ListItem {
757 marker: ListItemMarker::Dots(Span {
758 data: ".",
759 line: 5,
760 col: 1,
761 offset: 31,
762 },),
763 blocks: &[Block::Simple(SimpleBlock {
764 content: Content {
765 original: Span {
766 data: "Boo",
767 line: 5,
768 col: 3,
769 offset: 33,
770 },
771 rendered: "Boo",
772 },
773 source: Span {
774 data: "Boo",
775 line: 5,
776 col: 3,
777 offset: 33,
778 },
779 style: SimpleBlockStyle::Paragraph,
780 title_source: None,
781 title: None,
782 anchor: None,
783 anchor_reftext: None,
784 attrlist: None,
785 },),],
786 source: Span {
787 data: ". Boo",
788 line: 5,
789 col: 1,
790 offset: 31,
791 },
792 anchor: None,
793 anchor_reftext: None,
794 attrlist: None,
795 },),],
796 source: Span {
797 data: "[loweralpha]\n\n[[anchor]]\n. Boo",
798 line: 2,
799 col: 1,
800 offset: 6,
801 },
802 title_source: None,
803 title: None,
804 anchor: Some(Span {
805 data: "anchor",
806 line: 4,
807 col: 3,
808 offset: 22,
809 },),
810 anchor_reftext: None,
811 attrlist: Some(Attrlist {
812 attributes: &[ElementAttribute {
813 name: None,
814 value: "loweralpha",
815 shorthand_items: &["loweralpha"],
816 },],
817 anchor: None,
818 source: Span {
819 data: "loweralpha",
820 line: 2,
821 col: 2,
822 offset: 7,
823 },
824 },),
825 },),
826 ],
827 source: Span {
828 data: "* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo",
829 line: 1,
830 col: 1,
831 offset: 0,
832 },
833 anchor: None,
834 anchor_reftext: None,
835 attrlist: None,
836 },),
837 Block::ListItem(ListItem {
838 marker: ListItemMarker::Asterisks(Span {
839 data: "*",
840 line: 6,
841 col: 1,
842 offset: 37,
843 },),
844 blocks: &[Block::Simple(SimpleBlock {
845 content: Content {
846 original: Span {
847 data: "Blech",
848 line: 6,
849 col: 3,
850 offset: 39,
851 },
852 rendered: "Blech",
853 },
854 source: Span {
855 data: "Blech",
856 line: 6,
857 col: 3,
858 offset: 39,
859 },
860 style: SimpleBlockStyle::Paragraph,
861 title_source: None,
862 title: None,
863 anchor: None,
864 anchor_reftext: None,
865 attrlist: None,
866 },),],
867 source: Span {
868 data: "* Blech",
869 line: 6,
870 col: 1,
871 offset: 37,
872 },
873 anchor: None,
874 anchor_reftext: None,
875 attrlist: None,
876 },),
877 ],
878 source: Span {
879 data: "* Foo\n[loweralpha]\n\n[[anchor]]\n. Boo\n* Blech",
880 line: 1,
881 col: 1,
882 offset: 0,
883 },
884 title_source: None,
885 title: None,
886 anchor: None,
887 anchor_reftext: None,
888 attrlist: None,
889 }
890 );
891 }
892
893 #[test]
894 fn parent_marker_after_metadata_separated_by_empty_lines() {
895 let list =
902 list_parse("* grandparent\n** parent\n*** nested\n[[anchor]]\n\n* back to grandparent")
903 .unwrap();
904
905 assert_eq!(list.item.nested_blocks().count(), 2);
907 assert_eq!(list.item.type_(), ListType::Unordered);
908
909 let mut outer_items = list.item.nested_blocks();
910
911 let first_outer = outer_items.next().unwrap();
913 let first_outer_blocks: Vec<_> = first_outer.nested_blocks().collect();
914 assert_eq!(first_outer_blocks.len(), 2); let nested_list = &first_outer_blocks[1];
918 assert_eq!(nested_list.nested_blocks().count(), 1);
919
920 let parent_item = nested_list.nested_blocks().next().unwrap();
922 let parent_blocks: Vec<_> = parent_item.nested_blocks().collect();
923 assert_eq!(parent_blocks.len(), 2); let innermost_list = &parent_blocks[1];
927 assert_eq!(innermost_list.nested_blocks().count(), 1);
928
929 let innermost_item = innermost_list.nested_blocks().next().unwrap();
931 assert_eq!(innermost_item.nested_blocks().count(), 1);
932
933 let second_outer = outer_items.next().unwrap();
935 assert_eq!(second_outer.nested_blocks().count(), 1);
936 assert!(outer_items.next().is_none());
937 }
938
939 #[test]
940 fn marker_style_single_dot() {
941 let list = list_parse(". Item one\n. Item two\n").unwrap();
942 assert_eq!(list.item.marker_style(), Some("arabic"));
943 }
944
945 #[test]
946 fn marker_style_double_dots() {
947 let list = list_parse(".. Item a\n.. Item b\n").unwrap();
948 assert_eq!(list.item.marker_style(), Some("loweralpha"));
949 }
950
951 #[test]
952 fn marker_style_triple_dots() {
953 let list = list_parse("... Item i\n... Item ii\n").unwrap();
954 assert_eq!(list.item.marker_style(), Some("lowerroman"));
955 }
956
957 #[test]
958 fn marker_style_four_dots() {
959 let list = list_parse(".... Item A\n.... Item B\n").unwrap();
960 assert_eq!(list.item.marker_style(), Some("upperalpha"));
961 }
962
963 #[test]
964 fn marker_style_five_dots() {
965 let list = list_parse("..... Item I\n..... Item II\n").unwrap();
966 assert_eq!(list.item.marker_style(), Some("upperroman"));
967 }
968
969 #[test]
970 fn marker_style_hyphen_returns_none() {
971 let list = list_parse("- Item one\n- Item two\n").unwrap();
972 assert_eq!(list.item.marker_style(), None);
973 }
974
975 #[test]
976 fn marker_style_asterisk_returns_none() {
977 let list = list_parse("* Item one\n* Item two\n").unwrap();
978 assert_eq!(list.item.marker_style(), None);
979 }
980
981 #[test]
982 fn marker_with_no_content() {
983 assert!(list_parse("- ").is_none());
987 assert!(list_parse("* ").is_none());
988 assert!(list_parse(". ").is_none());
989 }
990
991 #[test]
992 fn orphaned_title_after_continuation_is_discarded() {
993 let list = list_parse("* item one\n+\n.Title\n\nsecond paragraph").unwrap();
1000
1001 let mut items = list.item.nested_blocks();
1003 let item = items.next().unwrap();
1004 assert!(items.next().is_none());
1005
1006 let blocks: Vec<_> = item.nested_blocks().collect();
1009 assert_eq!(blocks.len(), 2);
1010
1011 assert_eq!(
1013 blocks[0],
1014 &Block::Simple(SimpleBlock {
1015 content: Content {
1016 original: Span {
1017 data: "item one",
1018 line: 1,
1019 col: 3,
1020 offset: 2,
1021 },
1022 rendered: "item one",
1023 },
1024 source: Span {
1025 data: "item one",
1026 line: 1,
1027 col: 3,
1028 offset: 2,
1029 },
1030 style: SimpleBlockStyle::Paragraph,
1031 title_source: None,
1032 title: None,
1033 anchor: None,
1034 anchor_reftext: None,
1035 attrlist: None,
1036 })
1037 );
1038
1039 assert_eq!(
1041 blocks[1],
1042 &Block::Simple(SimpleBlock {
1043 content: Content {
1044 original: Span {
1045 data: "second paragraph",
1046 line: 5,
1047 col: 1,
1048 offset: 21,
1049 },
1050 rendered: "second paragraph",
1051 },
1052 source: Span {
1053 data: "second paragraph",
1054 line: 5,
1055 col: 1,
1056 offset: 21,
1057 },
1058 style: SimpleBlockStyle::Paragraph,
1059 title_source: None,
1060 title: None,
1061 anchor: None,
1062 anchor_reftext: None,
1063 attrlist: None,
1064 })
1065 );
1066 }
1067
1068 #[test]
1069 fn block_list_enum_case() {
1070 let mut parser = crate::Parser::default();
1071
1072 let mi = crate::blocks::Block::parse(crate::Span::new("- blah"), &mut parser)
1073 .unwrap_if_no_warnings()
1074 .unwrap();
1075
1076 assert!(matches!(mi.item, crate::blocks::Block::List(_)));
1077
1078 assert_eq!(mi.item.content_model(), ContentModel::Compound);
1079 assert!(mi.item.rendered_content().is_none());
1080 assert_eq!(mi.item.raw_context().as_ref(), "list");
1081 assert_eq!(mi.item.nested_blocks().count(), 1);
1082 assert!(mi.item.title_source().is_none());
1083 assert!(mi.item.title().is_none());
1084 assert!(mi.item.anchor().is_none());
1085 assert!(mi.item.anchor_reftext().is_none());
1086 assert!(mi.item.attrlist().is_none());
1087 assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
1088
1089 assert_eq!(
1090 mi.item.span(),
1091 Span {
1092 data: "- blah",
1093 line: 1,
1094 col: 1,
1095 offset: 0,
1096 }
1097 );
1098
1099 let debug_str = format!("{:?}", mi.item);
1100 assert!(debug_str.starts_with("Block::List("));
1101 }
1102}