asciidoc_parser/blocks/list_item.rs
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, block::BlockParseOutcome, metadata::BlockMetadata,
9 },
10 content::Content,
11 internal::debug::DebugSliceReference,
12 span::MatchedItem,
13 strings::CowStr,
14 warnings::Warning,
15};
16
17/// A list item is a special kind of block that contains one or more blocks
18/// attached to it. In the simplest case, this will be a single [`SimpleBlock`]
19/// with the principal text for the list item. In other cases, it may be any
20/// number of blocks of any type which, together, form an entry in a list which
21/// is the immediate parent of this block.
22///
23/// [`SimpleBlock`]: crate::blocks::SimpleBlock
24#[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 checkbox: Option<bool>,
33}
34
35impl<'src> ListItem<'src> {
36 pub(crate) fn parse(
37 metadata: &BlockMetadata<'src>,
38 parent_list_markers: &[ListItemMarker<'src>],
39 is_bibliography: bool,
40 parser: &mut Parser,
41 warnings: &mut Vec<Warning<'src>>,
42 ) -> Option<MatchedItem<'src, Self>> {
43 let source = metadata.block_start.discard_empty_lines();
44
45 let marker_mi = ListItemMarker::parse(source, parser)?;
46 let mut marker = marker_mi.item;
47
48 // Register any leading inline anchors in the description list term and apply
49 // macros substitution to render the anchor.
50 marker.register_leading_anchors(parser, warnings);
51
52 let mut list_markers_including_peer = parent_list_markers.to_vec();
53 list_markers_including_peer.push(marker.clone());
54
55 let mut blocks: Vec<Block<'src>> = vec![];
56
57 // Detect checklist (i.e. task list) syntax on unordered list items. When
58 // the principal text begins with `[ ] `, `[x] `, or `[*] ` (the closing
59 // bracket immediately followed by a single space character), the item is
60 // a checklist item. `[ ]` is unchecked; `[x]`/`[*]` are checked. The
61 // four-character checkbox marker is then stripped from the principal text.
62 // This mirrors Asciidoctor's `parse_list_item`.
63 let checkbox: Option<bool> = if matches!(
64 marker,
65 ListItemMarker::Hyphen(_) | ListItemMarker::Asterisks(_) | ListItemMarker::Bullet(_)
66 ) {
67 let text = marker_mi.after.data();
68 if text.starts_with("[ ] ") {
69 Some(false)
70 } else if text.starts_with("[x] ") || text.starts_with("[*] ") {
71 Some(true)
72 } else {
73 None
74 }
75 } else {
76 None
77 };
78
79 // When a checkbox marker is present, the principal text begins four
80 // characters later (after `[x] `). Any whitespace re-exposed by stripping
81 // the checkbox is discarded so the principal text never starts indented
82 // (which would otherwise be misread as a literal block).
83 let principal_start = if checkbox.is_some() {
84 marker_mi.after.slice_from(4..).discard_whitespace()
85 } else {
86 marker_mi.after
87 };
88
89 // Text after list item marker is always a simple block with no metadata.
90 let no_metadata = BlockMetadata {
91 title_source: None,
92 title: None,
93 anchor: None,
94 anchor_reftext: None,
95 attrlist: None,
96 source: principal_start,
97 block_start: principal_start,
98 };
99
100 // In a bibliography list, the principal text may begin with a
101 // bibliography anchor (`[[[id]]]`). Flag the parser so the inline macros
102 // substitution applied while parsing this principal text recognizes it.
103 // The flag is cleared immediately afterward so it never leaks into any
104 // nested blocks (e.g. a nested list) attached to this item.
105 parser.in_bibliography_list_item.set(is_bibliography);
106
107 // For description lists, the content after the marker can be empty.
108 // For other list types, we require content.
109 let simple_block_for_list_item = SimpleBlock::parse_for_list_item(
110 &no_metadata,
111 parser,
112 false,
113 &list_markers_including_peer,
114 );
115
116 parser.in_bibliography_list_item.set(false);
117
118 let mut next = if let Some(simple_block_mi) = simple_block_for_list_item {
119 // If the principal text is empty (e.g. from {empty} attribute reference),
120 // drop it from the parse tree.
121 if !simple_block_mi.item.content().is_empty() {
122 blocks.push(Block::Simple(simple_block_mi.item));
123 }
124 simple_block_mi.after
125 } else if matches!(marker, ListItemMarker::DefinedTerm { .. }) {
126 // Description list items can have empty content on the same line as the marker.
127 // The content may be on subsequent lines, so we try to parse from the next
128 // non-empty line.
129 let mut next_source = marker_mi.after.discard_empty_lines();
130
131 // Skip comment lines (// but not ///) between term and continuation/content.
132 loop {
133 let peek = next_source.take_normalized_line();
134 if peek.item.data().starts_with("//") && !peek.item.data().starts_with("///") {
135 next_source = peek.after.discard_empty_lines();
136 } else {
137 break;
138 }
139 }
140
141 // Check for continuation marker before parsing. If a continuation marker is
142 // present, skip directly to the main loop which handles continuations properly.
143 let next_line_mi = next_source.take_normalized_line();
144
145 if next_line_mi.item.data() == "+" {
146 // Continuation marker found; skip straight to the main loop.
147 // Use next_source (not marker_mi.after) since we already skipped empty lines.
148 next_source
149 } else if ListItemMarker::parse(next_source, parser).is_some() {
150 // Next line is another list item marker (possibly a sibling term).
151 // Don't parse it as content; let the list parser handle it.
152 marker_mi.after
153 } else if RawDelimitedBlock::is_valid_delimiter(&next_line_mi.item)
154 || CompoundDelimitedBlock::is_valid_delimiter(&next_line_mi.item)
155 {
156 // Delimited block breaks the list.
157 marker_mi.after
158 } else if next_line_mi.item.data().starts_with('[')
159 && !next_line_mi.item.data().starts_with("[[")
160 && next_line_mi.item.data().ends_with(']')
161 {
162 // Block attribute line breaks the list.
163 marker_mi.after
164 } else if next_line_mi.item.data().starts_with("[[")
165 && next_line_mi.item.data().ends_with("]]")
166 {
167 // Block anchor line breaks the list.
168 marker_mi.after
169 } else {
170 let next_line_metadata = BlockMetadata {
171 title_source: None,
172 title: None,
173 anchor: None,
174 anchor_reftext: None,
175 attrlist: None,
176 source: next_source,
177 block_start: next_source,
178 };
179
180 // For definition lists, indented content is treated as a paragraph
181 // (not literal), with the indentation stripped.
182 if let Some(simple_block_mi) =
183 SimpleBlock::parse_for_definition_list(&next_line_metadata, parser)
184 {
185 blocks.push(Block::Simple(simple_block_mi.item));
186 simple_block_mi.after
187 } else {
188 marker_mi.after
189 }
190 }
191 } else {
192 // Other list types require content after the marker.
193 return None;
194 };
195
196 let mut next_block_must_be_indented = false;
197 let mut continuation_active = false;
198 let mut had_content_starting_with_plus = false;
199
200 loop {
201 if next.is_empty() {
202 break;
203 }
204
205 let next_line_mi: MatchedItem<'_, Span<'_>> = next.take_normalized_line();
206
207 // Don't consume `+` as continuation if:
208 // - A continuation is already active (consecutive `+` - second one becomes
209 // content)
210 // - We've already had a block that started with `+` as content (trailing `+`
211 // markers)
212 if next_line_mi.item.data() == "+"
213 && !continuation_active
214 && !had_content_starting_with_plus
215 {
216 next = next_line_mi.after;
217 next_block_must_be_indented = false;
218 continuation_active = true;
219 continue;
220 }
221
222 if next_line_mi.item.data().is_empty() {
223 if parent_list_markers.is_empty() {
224 next = next.discard_empty_lines();
225 next_block_must_be_indented = true;
226 continue;
227 } else if blocks.len() > 1 {
228 // Item already has content beyond principal text (e.g.,
229 // continuation-attached blocks or nested lists). Consume
230 // all blank lines at this level.
231 next = next.discard_empty_lines();
232 break;
233 } else {
234 // Item has only principal text. Consume one blank line
235 // per level to support ancestor list continuation, where
236 // each blank line signals moving up one nesting level.
237 next = next_line_mi.after;
238 break;
239 }
240 }
241
242 let is_indented = next.starts_with(' ') || next.starts_with('\t');
243 let metadata = BlockMetadata::parse(next, parser);
244
245 if let Some(list_item_marker_mi) =
246 ListItemMarker::parse(metadata.item.block_start, parser)
247 {
248 // We've found a new list item. How does it compare with the existing item in
249 // the hierarchy?
250 let new_item_marker = list_item_marker_mi.item;
251
252 if marker.is_match_for(&new_item_marker) {
253 // New item is a peer to this item; nothing further for the current item.
254 break;
255 }
256
257 if parent_list_markers
258 .iter()
259 .any(|parent| parent.is_match_for(&new_item_marker))
260 {
261 // We matched a parent marker type. This list is complete; roll up the
262 // hierarchy.
263 break;
264 }
265
266 // We haven't encountered this marker before. Add a new nesting level. The new
267 // list will be a child block of this list item.
268
269 // But if we're after a blank line and the block is not indented
270 // (and no continuation is active), and there is a block attribute
271 // line or anchor before the new list marker, break the list
272 // instead of nesting. A blank line followed by a block attribute
273 // line signals the start of a new, separate list.
274 if next_block_must_be_indented
275 && !is_indented
276 && !continuation_active
277 && !blocks.is_empty()
278 && (metadata.item.attrlist.is_some() || metadata.item.anchor.is_some())
279 {
280 break;
281 }
282
283 let mut nested_list_markers = parent_list_markers.to_owned();
284 nested_list_markers.push(marker.clone());
285
286 // NOTE: The call to `ListBlock::parse` *should* succeed (as in I can't think of
287 // a test case where it would fail). We use the `?` to provide a safe escape in
288 // case it doesn't.
289 let nested_list_mi = ListBlock::parse_inside_list(
290 &metadata.item,
291 &nested_list_markers,
292 parser,
293 warnings,
294 )?;
295
296 blocks.push(Block::List(nested_list_mi.item));
297
298 next = nested_list_mi.after;
299 continuation_active = false;
300 next_block_must_be_indented = true;
301 continue;
302 }
303
304 // If no list marker found directly after metadata, try extending
305 // metadata past empty lines. This handles block attribute lines
306 // (anchors, attrlists) separated by empty lines above nested lists.
307 if !metadata.item.is_empty() {
308 let mut ext_block_start = metadata.item.block_start;
309 let mut ext_anchor = metadata.item.anchor;
310 let mut ext_anchor_reftext = metadata.item.anchor_reftext;
311 let mut ext_attrlist = metadata.item.attrlist.clone();
312 let mut ext_title_source = metadata.item.title_source;
313 let mut ext_title = metadata.item.title.clone();
314
315 // Try to consume additional metadata past empty lines.
316 loop {
317 let gap = ext_block_start.discard_empty_lines();
318 if gap == ext_block_start {
319 break;
320 }
321
322 let more_maw = BlockMetadata::parse(gap, parser);
323 if more_maw.item.is_empty() {
324 ext_block_start = gap;
325 break;
326 }
327
328 // Merge additional metadata.
329 if ext_anchor.is_none() {
330 ext_anchor = more_maw.item.anchor;
331 ext_anchor_reftext = more_maw.item.anchor_reftext;
332 }
333
334 if ext_attrlist.is_none() {
335 ext_attrlist = more_maw.item.attrlist;
336 }
337
338 if ext_title_source.is_none() {
339 ext_title_source = more_maw.item.title_source;
340 ext_title = more_maw.item.title;
341 }
342
343 ext_block_start = more_maw.item.block_start;
344 }
345
346 if let Some(ext_marker_mi) = ListItemMarker::parse(ext_block_start, parser) {
347 let new_item_marker = ext_marker_mi.item;
348
349 if marker.is_match_for(&new_item_marker) {
350 next = ext_block_start;
351 break;
352 }
353
354 if parent_list_markers
355 .iter()
356 .any(|parent| parent.is_match_for(&new_item_marker))
357 {
358 next = ext_block_start;
359 break;
360 }
361
362 // Found a nested list after metadata separated by empty lines.
363 let ext_metadata = BlockMetadata {
364 title_source: ext_title_source,
365 title: ext_title,
366 anchor: ext_anchor,
367 anchor_reftext: ext_anchor_reftext,
368 attrlist: ext_attrlist,
369 source: metadata.item.source,
370 block_start: ext_block_start,
371 };
372
373 let mut nested_list_markers = parent_list_markers.to_owned();
374 nested_list_markers.push(marker.clone());
375
376 let nested_list_mi = ListBlock::parse_inside_list(
377 &ext_metadata,
378 &nested_list_markers,
379 parser,
380 warnings,
381 )?;
382
383 blocks.push(Block::List(nested_list_mi.item));
384
385 next = nested_list_mi.after;
386 continuation_active = false;
387 next_block_must_be_indented = true;
388 continue;
389 }
390 }
391
392 if next_block_must_be_indented && !is_indented {
393 break;
394 }
395
396 // A delimited block without a continuation marker breaks the list.
397 if !continuation_active {
398 let next_block_line = metadata.item.block_start.take_normalized_line().item;
399 if RawDelimitedBlock::is_valid_delimiter(&next_block_line)
400 || CompoundDelimitedBlock::is_valid_delimiter(&next_block_line)
401 {
402 break;
403 }
404 }
405
406 // A block attribute line or block anchor without a continuation marker
407 // breaks the list.
408 if !continuation_active
409 && (metadata.item.attrlist.is_some() || metadata.item.anchor.is_some())
410 {
411 break;
412 }
413
414 // If there's block metadata but no block, just discard it and continue.
415 if metadata
416 .item
417 .block_start
418 .take_normalized_line()
419 .item
420 .is_empty()
421 {
422 next = metadata.item.block_start.discard_empty_lines();
423 continue;
424 }
425
426 // A list item does not terminate if subsequent blocks are indented (i.e. use
427 // literal syntax).
428 let indented_block_maw = Block::parse_for_list_item(
429 next,
430 parser,
431 &list_markers_including_peer,
432 continuation_active,
433 );
434 warnings.extend(indented_block_maw.warnings);
435
436 // A block dropped at parse time (`attribute-missing=drop-line` on a
437 // block-macro target) attaches nothing, but — like a real block —
438 // it consumes any active continuation and requires the next block
439 // to be indented or reintroduced with a `+`. (A dropped block is
440 // always a block macro, never `+`-prefixed content, so it can't set
441 // `had_content_starting_with_plus`.)
442 if let BlockParseOutcome::Dropped(after) = indented_block_maw.item {
443 next = after;
444 continuation_active = false;
445 next_block_must_be_indented = true;
446 continue;
447 }
448
449 // `NoMatch` only arises for empty/blank input, which is filtered out
450 // above before we get here; the defensive `break` mirrors the
451 // pre-drop-line code path.
452 let BlockParseOutcome::Parsed(indented_block_mi) = indented_block_maw.item else {
453 break;
454 };
455
456 // After a continuation marker, subsequent blocks don't need to be indented.
457 // However, document attributes don't consume the continuation status.
458 let is_document_attribute =
459 matches!(indented_block_mi.item, Block::DocumentAttribute(_));
460
461 // Document attributes should not be added to the list item blocks.
462 // They're processed for their side effects but don't appear in the output.
463 // Similarly, orphaned metadata blocks shouldn't be added; they'll be
464 // re-parsed on the next iteration where they can attach to a real block.
465 if !is_document_attribute {
466 blocks.push(indented_block_mi.item);
467 }
468 next = indented_block_mi.after;
469
470 if is_document_attribute {
471 // Document attributes and orphaned metadata are transparent to
472 // continuation logic. Keep continuation_active
473 // and next_block_must_be_indented unchanged.
474 } else if continuation_active {
475 // This block consumed the continuation.
476 // The next block after this one will need to be indented (or have another
477 // continuation).
478 //
479 // If the block started with `+` as content (not as continuation), mark it
480 // so we don't allow more continuation markers. This handles odd input like
481 // consecutive `+` markers.
482 if next_line_mi.item.data() == "+" {
483 had_content_starting_with_plus = true;
484 }
485 continuation_active = false;
486 next_block_must_be_indented = true;
487 } else {
488 // No active continuation; next block must be indented.
489 next_block_must_be_indented = true;
490 }
491 }
492
493 let source = source.trim_remainder(next).trim_trailing_whitespace();
494
495 Some(MatchedItem {
496 item: Self {
497 marker,
498 blocks,
499 source,
500 anchor: metadata.anchor,
501 anchor_reftext: metadata.anchor_reftext,
502 attrlist: metadata.attrlist.clone(),
503 checkbox,
504 },
505 after: next,
506 })
507 }
508
509 /// Returns the list item marker that was used for this item.
510 pub fn list_item_marker(&self) -> ListItemMarker<'src> {
511 self.marker.clone()
512 }
513
514 /// Returns the checklist (i.e. task list) state of this item.
515 ///
516 /// An unordered list item whose principal text begins with a checkbox
517 /// marker (`[ ] `, `[x] `, or `[*] `) is a checklist item. The returned
518 /// value is `Some(true)` for a checked item (`[x]`/`[*]`), `Some(false)`
519 /// for an unchecked item (`[ ]`), or `None` if the item is not a checklist
520 /// item.
521 pub fn checkbox(&self) -> Option<bool> {
522 self.checkbox
523 }
524}
525
526impl<'src> IsBlock<'src> for ListItem<'src> {
527 fn content_model(&self) -> ContentModel {
528 ContentModel::Compound
529 }
530
531 fn content_mut(&mut self) -> Option<&mut Content<'src>> {
532 // A description-list item's resolvable content is its term.
533 self.marker.term_mut()
534 }
535
536 fn nested_blocks_mut(&mut self) -> &mut [Block<'src>] {
537 &mut self.blocks
538 }
539
540 fn raw_context(&self) -> CowStr<'src> {
541 "list_item".into()
542 }
543
544 fn nested_blocks(&'src self) -> Iter<'src, Block<'src>> {
545 self.blocks.iter()
546 }
547
548 fn title_source(&'src self) -> Option<Span<'src>> {
549 None
550 }
551
552 fn title(&self) -> Option<&str> {
553 None
554 }
555
556 fn anchor(&'src self) -> Option<Span<'src>> {
557 self.anchor
558 }
559
560 fn anchor_reftext(&'src self) -> Option<Span<'src>> {
561 self.anchor_reftext
562 }
563
564 fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
565 self.attrlist.as_ref()
566 }
567}
568
569impl<'src> HasSpan<'src> for ListItem<'src> {
570 fn span(&self) -> Span<'src> {
571 self.source
572 }
573}
574
575impl std::fmt::Debug for ListItem<'_> {
576 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
577 f.debug_struct("ListItem")
578 .field("marker", &self.marker)
579 .field("blocks", &DebugSliceReference(&self.blocks))
580 .field("source", &self.source)
581 .field("anchor", &self.anchor)
582 .field("anchor_reftext", &self.anchor_reftext)
583 .field("attrlist", &self.attrlist)
584 .field("checkbox", &self.checkbox)
585 .finish()
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 #![allow(clippy::panic)]
592 #![allow(clippy::unwrap_used)]
593
594 use crate::{
595 blocks::{ContentModel, metadata::BlockMetadata},
596 span::MatchedItem,
597 tests::prelude::*,
598 warnings::Warning,
599 };
600
601 fn li_parse<'a>(source: &'a str) -> Option<MatchedItem<'a, crate::blocks::ListItem<'a>>> {
602 let mut parser = crate::Parser::default();
603 let mut warnings: Vec<Warning<'a>> = vec![];
604
605 let metadata = BlockMetadata::parse(crate::Span::new(source), &mut parser).item;
606
607 let result = crate::blocks::list_item::ListItem::parse(
608 &metadata,
609 &[],
610 false,
611 &mut parser,
612 &mut warnings,
613 );
614
615 assert!(warnings.is_empty());
616
617 result
618 }
619
620 #[test]
621 fn hyphen() {
622 assert!(li_parse("-xyz").is_none());
623 assert!(li_parse("-- x").is_none());
624
625 let li = li_parse("- blah").unwrap();
626
627 assert_eq!(
628 li.item,
629 ListItem {
630 marker: ListItemMarker::Hyphen(Span {
631 data: "-",
632 line: 1,
633 col: 1,
634 offset: 0,
635 },),
636 blocks: &[Block::Simple(SimpleBlock {
637 content: Content {
638 original: Span {
639 data: "blah",
640 line: 1,
641 col: 3,
642 offset: 2,
643 },
644 rendered: "blah",
645 },
646 source: Span {
647 data: "blah",
648 line: 1,
649 col: 3,
650 offset: 2,
651 },
652 style: SimpleBlockStyle::Paragraph,
653 title_source: None,
654 title: None,
655 caption: None,
656 number: None,
657 anchor: None,
658 anchor_reftext: None,
659 attrlist: None,
660 },),],
661 source: Span {
662 data: "- blah",
663 line: 1,
664 col: 1,
665 offset: 0,
666 },
667 anchor: None,
668 anchor_reftext: None,
669 attrlist: None,
670 }
671 );
672
673 assert_eq!(li.item.content_model(), ContentModel::Compound);
674 assert_eq!(li.item.raw_context().as_ref(), "list_item");
675
676 let mut li_blocks = li.item.nested_blocks();
677
678 assert_eq!(
679 li_blocks.next().unwrap(),
680 &Block::Simple(SimpleBlock {
681 content: Content {
682 original: Span {
683 data: "blah",
684 line: 1,
685 col: 3,
686 offset: 2,
687 },
688 rendered: "blah",
689 },
690 source: Span {
691 data: "blah",
692 line: 1,
693 col: 3,
694 offset: 2,
695 },
696 style: SimpleBlockStyle::Paragraph,
697 title_source: None,
698 title: None,
699 caption: None,
700 number: None,
701 anchor: None,
702 anchor_reftext: None,
703 attrlist: None,
704 })
705 );
706 assert!(li_blocks.next().is_none());
707
708 assert!(li.item.title_source().is_none());
709 assert!(li.item.title().is_none());
710 assert!(li.item.anchor().is_none());
711 assert!(li.item.anchor_reftext().is_none());
712 assert!(li.item.attrlist().is_none());
713
714 assert_eq!(
715 li.item.span(),
716 Span {
717 data: "- blah",
718 line: 1,
719 col: 1,
720 offset: 0,
721 }
722 );
723
724 assert_eq!(
725 li.after,
726 Span {
727 data: "",
728 line: 1,
729 col: 7,
730 offset: 6,
731 }
732 );
733
734 assert_eq!(
735 format!("{:#?}", li.item),
736 "ListItem {\n marker: ListItemMarker::Hyphen(\n Span {\n data: \"-\",\n line: 1,\n col: 1,\n offset: 0,\n },\n ),\n blocks: &[\n Block::Simple(\n SimpleBlock {\n content: Content {\n original: Span {\n data: \"blah\",\n line: 1,\n col: 3,\n offset: 2,\n },\n rendered: \"blah\",\n },\n source: Span {\n data: \"blah\",\n line: 1,\n col: 3,\n offset: 2,\n },\n style: SimpleBlockStyle::Paragraph,\n title_source: None,\n title: None,\n caption: None,\n number: None,\n anchor: None,\n anchor_reftext: None,\n attrlist: None,\n },\n ),\n ],\n source: Span {\n data: \"- blah\",\n line: 1,\n col: 1,\n offset: 0,\n },\n anchor: None,\n anchor_reftext: None,\n attrlist: None,\n checkbox: None,\n}"
737 );
738 }
739
740 #[test]
741 fn non_description_list_marker_with_no_content() {
742 // A non-description-list marker with no content after it returns None.
743 assert!(li_parse("* ").is_none());
744 }
745
746 #[test]
747 fn asterisks() {
748 assert!(li_parse("*").is_none());
749 assert!(li_parse("*xyz").is_none());
750 assert!(li_parse("*- xyz").is_none());
751
752 let li = li_parse("* blah").unwrap();
753
754 assert_eq!(
755 li.item,
756 ListItem {
757 marker: ListItemMarker::Asterisks(Span {
758 data: "*",
759 line: 1,
760 col: 1,
761 offset: 0,
762 },),
763 blocks: &[Block::Simple(SimpleBlock {
764 content: Content {
765 original: Span {
766 data: "blah",
767 line: 1,
768 col: 3,
769 offset: 2,
770 },
771 rendered: "blah",
772 },
773 source: Span {
774 data: "blah",
775 line: 1,
776 col: 3,
777 offset: 2,
778 },
779 style: SimpleBlockStyle::Paragraph,
780 title_source: None,
781 title: None,
782 caption: None,
783 number: None,
784 anchor: None,
785 anchor_reftext: None,
786 attrlist: None,
787 },),],
788 source: Span {
789 data: "* blah",
790 line: 1,
791 col: 1,
792 offset: 0,
793 },
794 anchor: None,
795 anchor_reftext: None,
796 attrlist: None,
797 }
798 );
799
800 assert_eq!(
801 li.item.span(),
802 Span {
803 data: "* blah",
804 line: 1,
805 col: 1,
806 offset: 0,
807 }
808 );
809
810 assert_eq!(
811 li.after,
812 Span {
813 data: "",
814 line: 1,
815 col: 7,
816 offset: 6,
817 }
818 );
819 }
820}