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