1use std::slice::Iter;
2
3use crate::{
4 HasSpan, Parser, Span,
5 attributes::Attrlist,
6 blocks::{
7 Block, CompoundDelimitedBlock, ContentModel, IsBlock, ListBlock, ListItemMarker,
8 RawDelimitedBlock, SimpleBlock, metadata::BlockMetadata,
9 },
10 content::Content,
11 internal::debug::DebugSliceReference,
12 span::MatchedItem,
13 strings::CowStr,
14 warnings::Warning,
15};
16
17#[derive(Clone, Eq, PartialEq)]
25pub struct ListItem<'src> {
26 marker: ListItemMarker<'src>,
27 blocks: Vec<Block<'src>>,
28 source: Span<'src>,
29 anchor: Option<Span<'src>>,
30 anchor_reftext: Option<Span<'src>>,
31 attrlist: Option<Attrlist<'src>>,
32}
33
34impl<'src> ListItem<'src> {
35 pub(crate) fn parse(
36 metadata: &BlockMetadata<'src>,
37 parent_list_markers: &[ListItemMarker<'src>],
38 parser: &mut Parser,
39 warnings: &mut Vec<Warning<'src>>,
40 ) -> Option<MatchedItem<'src, Self>> {
41 let source = metadata.block_start.discard_empty_lines();
42
43 let marker_mi = ListItemMarker::parse(source, parser)?;
44 let mut marker = marker_mi.item;
45
46 marker.register_leading_anchors(parser, warnings);
49
50 let mut list_markers_including_peer = parent_list_markers.to_vec();
51 list_markers_including_peer.push(marker.clone());
52
53 let mut blocks: Vec<Block<'src>> = vec![];
54
55 let no_metadata = BlockMetadata {
57 title_source: None,
58 title: None,
59 anchor: None,
60 anchor_reftext: None,
61 attrlist: None,
62 source: marker_mi.after,
63 block_start: marker_mi.after,
64 };
65
66 let mut next = if let Some(simple_block_mi) = SimpleBlock::parse_for_list_item(
69 &no_metadata,
70 parser,
71 false,
72 &list_markers_including_peer,
73 ) {
74 if !simple_block_mi.item.content().is_empty() {
77 blocks.push(Block::Simple(simple_block_mi.item));
78 }
79 simple_block_mi.after
80 } else if matches!(marker, ListItemMarker::DefinedTerm { .. }) {
81 let mut next_source = marker_mi.after.discard_empty_lines();
85
86 loop {
88 let peek = next_source.take_normalized_line();
89 if peek.item.data().starts_with("//") && !peek.item.data().starts_with("///") {
90 next_source = peek.after.discard_empty_lines();
91 } else {
92 break;
93 }
94 }
95
96 let next_line_mi = next_source.take_normalized_line();
99
100 if next_line_mi.item.data() == "+" {
101 next_source
104 } else if ListItemMarker::parse(next_source, parser).is_some() {
105 marker_mi.after
108 } else if RawDelimitedBlock::is_valid_delimiter(&next_line_mi.item)
109 || CompoundDelimitedBlock::is_valid_delimiter(&next_line_mi.item)
110 {
111 marker_mi.after
113 } else if next_line_mi.item.data().starts_with('[')
114 && !next_line_mi.item.data().starts_with("[[")
115 && next_line_mi.item.data().ends_with(']')
116 {
117 marker_mi.after
119 } else if next_line_mi.item.data().starts_with("[[")
120 && next_line_mi.item.data().ends_with("]]")
121 {
122 marker_mi.after
124 } else {
125 let next_line_metadata = BlockMetadata {
126 title_source: None,
127 title: None,
128 anchor: None,
129 anchor_reftext: None,
130 attrlist: None,
131 source: next_source,
132 block_start: next_source,
133 };
134
135 if let Some(simple_block_mi) =
138 SimpleBlock::parse_for_definition_list(&next_line_metadata, parser)
139 {
140 blocks.push(Block::Simple(simple_block_mi.item));
141 simple_block_mi.after
142 } else {
143 marker_mi.after
144 }
145 }
146 } else {
147 return None;
149 };
150
151 let mut next_block_must_be_indented = false;
152 let mut continuation_active = false;
153 let mut had_content_starting_with_plus = false;
154
155 loop {
156 if next.is_empty() {
157 break;
158 }
159
160 let next_line_mi: MatchedItem<'_, Span<'_>> = next.take_normalized_line();
161
162 if next_line_mi.item.data() == "+"
168 && !continuation_active
169 && !had_content_starting_with_plus
170 {
171 next = next_line_mi.after;
172 next_block_must_be_indented = false;
173 continuation_active = true;
174 continue;
175 }
176
177 if next_line_mi.item.data().is_empty() {
178 if parent_list_markers.is_empty() {
179 next = next.discard_empty_lines();
180 next_block_must_be_indented = true;
181 continue;
182 } else if blocks.len() > 1 {
183 next = next.discard_empty_lines();
187 break;
188 } else {
189 next = next_line_mi.after;
193 break;
194 }
195 }
196
197 let is_indented = next.starts_with(' ') || next.starts_with('\t');
198 let metadata = BlockMetadata::parse(next, parser);
199
200 if let Some(list_item_marker_mi) =
201 ListItemMarker::parse(metadata.item.block_start, parser)
202 {
203 let new_item_marker = list_item_marker_mi.item;
206
207 if marker.is_match_for(&new_item_marker) {
208 break;
210 }
211
212 if parent_list_markers
213 .iter()
214 .any(|parent| parent.is_match_for(&new_item_marker))
215 {
216 break;
219 }
220
221 if next_block_must_be_indented
230 && !is_indented
231 && !continuation_active
232 && !blocks.is_empty()
233 && (metadata.item.attrlist.is_some() || metadata.item.anchor.is_some())
234 {
235 break;
236 }
237
238 let mut nested_list_markers = parent_list_markers.to_owned();
239 nested_list_markers.push(marker.clone());
240
241 let nested_list_mi = ListBlock::parse_inside_list(
245 &metadata.item,
246 &nested_list_markers,
247 parser,
248 warnings,
249 )?;
250
251 blocks.push(Block::List(nested_list_mi.item));
252
253 next = nested_list_mi.after;
254 continuation_active = false;
255 next_block_must_be_indented = true;
256 continue;
257 }
258
259 if !metadata.item.is_empty() {
263 let mut ext_block_start = metadata.item.block_start;
264 let mut ext_anchor = metadata.item.anchor;
265 let mut ext_anchor_reftext = metadata.item.anchor_reftext;
266 let mut ext_attrlist = metadata.item.attrlist.clone();
267 let mut ext_title_source = metadata.item.title_source;
268 let mut ext_title = metadata.item.title.clone();
269
270 loop {
272 let gap = ext_block_start.discard_empty_lines();
273 if gap == ext_block_start {
274 break;
275 }
276
277 let more_maw = BlockMetadata::parse(gap, parser);
278 if more_maw.item.is_empty() {
279 ext_block_start = gap;
280 break;
281 }
282
283 if ext_anchor.is_none() {
285 ext_anchor = more_maw.item.anchor;
286 ext_anchor_reftext = more_maw.item.anchor_reftext;
287 }
288
289 if ext_attrlist.is_none() {
290 ext_attrlist = more_maw.item.attrlist;
291 }
292
293 if ext_title_source.is_none() {
294 ext_title_source = more_maw.item.title_source;
295 ext_title = more_maw.item.title;
296 }
297
298 ext_block_start = more_maw.item.block_start;
299 }
300
301 if let Some(ext_marker_mi) = ListItemMarker::parse(ext_block_start, parser) {
302 let new_item_marker = ext_marker_mi.item;
303
304 if marker.is_match_for(&new_item_marker) {
305 next = ext_block_start;
306 break;
307 }
308
309 if parent_list_markers
310 .iter()
311 .any(|parent| parent.is_match_for(&new_item_marker))
312 {
313 next = ext_block_start;
314 break;
315 }
316
317 let ext_metadata = BlockMetadata {
319 title_source: ext_title_source,
320 title: ext_title,
321 anchor: ext_anchor,
322 anchor_reftext: ext_anchor_reftext,
323 attrlist: ext_attrlist,
324 source: metadata.item.source,
325 block_start: ext_block_start,
326 };
327
328 let mut nested_list_markers = parent_list_markers.to_owned();
329 nested_list_markers.push(marker.clone());
330
331 let nested_list_mi = ListBlock::parse_inside_list(
332 &ext_metadata,
333 &nested_list_markers,
334 parser,
335 warnings,
336 )?;
337
338 blocks.push(Block::List(nested_list_mi.item));
339
340 next = nested_list_mi.after;
341 continuation_active = false;
342 next_block_must_be_indented = true;
343 continue;
344 }
345 }
346
347 if next_block_must_be_indented && !is_indented {
348 break;
349 }
350
351 if !continuation_active {
353 let next_block_line = metadata.item.block_start.take_normalized_line().item;
354 if RawDelimitedBlock::is_valid_delimiter(&next_block_line)
355 || CompoundDelimitedBlock::is_valid_delimiter(&next_block_line)
356 {
357 break;
358 }
359 }
360
361 if !continuation_active
364 && (metadata.item.attrlist.is_some() || metadata.item.anchor.is_some())
365 {
366 break;
367 }
368
369 if metadata
371 .item
372 .block_start
373 .take_normalized_line()
374 .item
375 .is_empty()
376 {
377 next = metadata.item.block_start.discard_empty_lines();
378 continue;
379 }
380
381 let indented_block_maw = Block::parse_for_list_item(
384 next,
385 parser,
386 &list_markers_including_peer,
387 continuation_active,
388 );
389 warnings.extend(indented_block_maw.warnings);
390
391 let Some(indented_block_mi) = indented_block_maw.item else {
392 break;
393 };
394
395 let is_document_attribute =
398 matches!(indented_block_mi.item, Block::DocumentAttribute(_));
399
400 if !is_document_attribute {
405 blocks.push(indented_block_mi.item);
406 }
407 next = indented_block_mi.after;
408
409 if is_document_attribute {
410 } else if continuation_active {
414 if next_line_mi.item.data() == "+" {
422 had_content_starting_with_plus = true;
423 }
424 continuation_active = false;
425 next_block_must_be_indented = true;
426 } else {
427 next_block_must_be_indented = true;
429 }
430 }
431
432 let source = source.trim_remainder(next).trim_trailing_whitespace();
433
434 Some(MatchedItem {
435 item: Self {
436 marker,
437 blocks,
438 source,
439 anchor: metadata.anchor,
440 anchor_reftext: metadata.anchor_reftext,
441 attrlist: metadata.attrlist.clone(),
442 },
443 after: next,
444 })
445 }
446
447 pub fn list_item_marker(&self) -> ListItemMarker<'src> {
449 self.marker.clone()
450 }
451}
452
453impl<'src> IsBlock<'src> for ListItem<'src> {
454 fn content_model(&self) -> ContentModel {
455 ContentModel::Compound
456 }
457
458 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
459 self.marker.term_mut()
461 }
462
463 fn nested_blocks_mut(&mut self) -> &mut [Block<'src>] {
464 &mut self.blocks
465 }
466
467 fn raw_context(&self) -> CowStr<'src> {
468 "list_item".into()
469 }
470
471 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
472 self.blocks.iter()
473 }
474
475 fn title_source(&'src self) -> Option<Span<'src>> {
476 None
477 }
478
479 fn title(&self) -> Option<&str> {
480 None
481 }
482
483 fn anchor(&'src self) -> Option<Span<'src>> {
484 self.anchor
485 }
486
487 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
488 self.anchor_reftext
489 }
490
491 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
492 self.attrlist.as_ref()
493 }
494}
495
496impl<'src> HasSpan<'src> for ListItem<'src> {
497 fn span(&self) -> Span<'src> {
498 self.source
499 }
500}
501
502impl std::fmt::Debug for ListItem<'_> {
503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504 f.debug_struct("ListItem")
505 .field("marker", &self.marker)
506 .field("blocks", &DebugSliceReference(&self.blocks))
507 .field("source", &self.source)
508 .field("anchor", &self.anchor)
509 .field("anchor_reftext", &self.anchor_reftext)
510 .field("attrlist", &self.attrlist)
511 .finish()
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 #![allow(clippy::panic)]
518 #![allow(clippy::unwrap_used)]
519
520 use crate::{
521 blocks::{ContentModel, metadata::BlockMetadata},
522 span::MatchedItem,
523 tests::prelude::*,
524 warnings::Warning,
525 };
526
527 fn li_parse<'a>(source: &'a str) -> Option<MatchedItem<'a, crate::blocks::ListItem<'a>>> {
528 let mut parser = crate::Parser::default();
529 let mut warnings: Vec<Warning<'a>> = vec![];
530
531 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
532
533 let result =
534 crate::blocks::list_item::ListItem::parse(&metadata, &[], &mut parser, &mut warnings);
535
536 assert!(warnings.is_empty());
537
538 result
539 }
540
541 #[test]
542 fn hyphen() {
543 assert!(li_parse("-xyz").is_none());
544 assert!(li_parse("-- x").is_none());
545
546 let li = li_parse("- blah").unwrap();
547
548 assert_eq!(
549 li.item,
550 ListItem {
551 marker: ListItemMarker::Hyphen(Span {
552 data: "-",
553 line: 1,
554 col: 1,
555 offset: 0,
556 },),
557 blocks: &[Block::Simple(SimpleBlock {
558 content: Content {
559 original: Span {
560 data: "blah",
561 line: 1,
562 col: 3,
563 offset: 2,
564 },
565 rendered: "blah",
566 },
567 source: Span {
568 data: "blah",
569 line: 1,
570 col: 3,
571 offset: 2,
572 },
573 style: SimpleBlockStyle::Paragraph,
574 title_source: None,
575 title: None,
576 caption: None,
577 number: None,
578 anchor: None,
579 anchor_reftext: None,
580 attrlist: None,
581 },),],
582 source: Span {
583 data: "- blah",
584 line: 1,
585 col: 1,
586 offset: 0,
587 },
588 anchor: None,
589 anchor_reftext: None,
590 attrlist: None,
591 }
592 );
593
594 assert_eq!(li.item.content_model(), ContentModel::Compound);
595 assert_eq!(li.item.raw_context().as_ref(), "list_item");
596
597 let mut li_blocks = li.item.nested_blocks();
598
599 assert_eq!(
600 li_blocks.next().unwrap(),
601 &Block::Simple(SimpleBlock {
602 content: Content {
603 original: Span {
604 data: "blah",
605 line: 1,
606 col: 3,
607 offset: 2,
608 },
609 rendered: "blah",
610 },
611 source: Span {
612 data: "blah",
613 line: 1,
614 col: 3,
615 offset: 2,
616 },
617 style: SimpleBlockStyle::Paragraph,
618 title_source: None,
619 title: None,
620 caption: None,
621 number: None,
622 anchor: None,
623 anchor_reftext: None,
624 attrlist: None,
625 })
626 );
627 assert!(li_blocks.next().is_none());
628
629 assert!(li.item.title_source().is_none());
630 assert!(li.item.title().is_none());
631 assert!(li.item.anchor().is_none());
632 assert!(li.item.anchor_reftext().is_none());
633 assert!(li.item.attrlist().is_none());
634
635 assert_eq!(
636 li.item.span(),
637 Span {
638 data: "- blah",
639 line: 1,
640 col: 1,
641 offset: 0,
642 }
643 );
644
645 assert_eq!(
646 li.after,
647 Span {
648 data: "",
649 line: 1,
650 col: 7,
651 offset: 6,
652 }
653 );
654
655 assert_eq!(
656 format!("{:#?}", li.item),
657 "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}"
658 );
659 }
660
661 #[test]
662 fn non_description_list_marker_with_no_content() {
663 assert!(li_parse("* ").is_none());
665 }
666
667 #[test]
668 fn asterisks() {
669 assert!(li_parse("*").is_none());
670 assert!(li_parse("*xyz").is_none());
671 assert!(li_parse("*- xyz").is_none());
672
673 let li = li_parse("* blah").unwrap();
674
675 assert_eq!(
676 li.item,
677 ListItem {
678 marker: ListItemMarker::Asterisks(Span {
679 data: "*",
680 line: 1,
681 col: 1,
682 offset: 0,
683 },),
684 blocks: &[Block::Simple(SimpleBlock {
685 content: Content {
686 original: Span {
687 data: "blah",
688 line: 1,
689 col: 3,
690 offset: 2,
691 },
692 rendered: "blah",
693 },
694 source: Span {
695 data: "blah",
696 line: 1,
697 col: 3,
698 offset: 2,
699 },
700 style: SimpleBlockStyle::Paragraph,
701 title_source: None,
702 title: None,
703 caption: None,
704 number: None,
705 anchor: None,
706 anchor_reftext: None,
707 attrlist: None,
708 },),],
709 source: Span {
710 data: "* blah",
711 line: 1,
712 col: 1,
713 offset: 0,
714 },
715 anchor: None,
716 anchor_reftext: None,
717 attrlist: None,
718 }
719 );
720
721 assert_eq!(
722 li.item.span(),
723 Span {
724 data: "* blah",
725 line: 1,
726 col: 1,
727 offset: 0,
728 }
729 );
730
731 assert_eq!(
732 li.after,
733 Span {
734 data: "",
735 line: 1,
736 col: 7,
737 offset: 6,
738 }
739 );
740 }
741}