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