@@ -2497,11 +2497,56 @@ struct MarkdownRenderer {
24972497 in_code_block : bool ,
24982498 code_block_lang : Option < String > ,
24992499 code_block_buf : String ,
2500- list_prefix : Option < String > ,
2500+ item_prefix : Option < ListPrefix > ,
25012501 pending_space : bool ,
25022502 active_link_url : Option < String > ,
25032503}
25042504
2505+ #[ derive( Debug , Clone ) ]
2506+ struct ListPrefix {
2507+ first_line : String ,
2508+ continuation : String ,
2509+ first_line_pending : bool ,
2510+ }
2511+
2512+ impl ListPrefix {
2513+ fn bullet ( ) -> Self {
2514+ Self :: new ( "• " . to_string ( ) )
2515+ }
2516+
2517+ fn task ( checked : bool ) -> Self {
2518+ let prefix = if checked { "[x] " } else { "[ ] " } ;
2519+ Self :: new ( prefix. to_string ( ) )
2520+ }
2521+
2522+ fn new ( first_line : String ) -> Self {
2523+ let continuation = " " . repeat ( display_width ( & first_line) ) ;
2524+ Self {
2525+ first_line,
2526+ continuation,
2527+ first_line_pending : true ,
2528+ }
2529+ }
2530+
2531+ fn current_text ( & self ) -> & str {
2532+ if self . first_line_pending {
2533+ & self . first_line
2534+ } else {
2535+ & self . continuation
2536+ }
2537+ }
2538+
2539+ fn current_width ( & self ) -> usize {
2540+ display_width ( self . current_text ( ) )
2541+ }
2542+
2543+ fn take_for_line ( & mut self ) -> String {
2544+ let prefix = self . current_text ( ) . to_string ( ) ;
2545+ self . first_line_pending = false ;
2546+ prefix
2547+ }
2548+ }
2549+
25052550#[ derive( Clone , Copy ) ]
25062551struct AdmonitionStyle {
25072552 marker : & ' static str ,
@@ -2564,7 +2609,7 @@ impl MarkdownRenderer {
25642609 in_code_block : false ,
25652610 code_block_lang : None ,
25662611 code_block_buf : String :: new ( ) ,
2567- list_prefix : None ,
2612+ item_prefix : None ,
25682613 pending_space : false ,
25692614 active_link_url : None ,
25702615 }
@@ -2604,7 +2649,7 @@ impl MarkdownRenderer {
26042649 }
26052650 Tag :: Item => {
26062651 self . flush_line ( ) ;
2607- self . list_prefix = Some ( "• " . to_string ( ) ) ;
2652+ self . item_prefix = Some ( ListPrefix :: bullet ( ) ) ;
26082653 }
26092654 _ => { }
26102655 }
@@ -2644,7 +2689,7 @@ impl MarkdownRenderer {
26442689 }
26452690 TagEnd :: Item => {
26462691 self . flush_line ( ) ;
2647- self . list_prefix = None ;
2692+ self . item_prefix = None ;
26482693 }
26492694 TagEnd :: Paragraph => {
26502695 self . flush_line ( ) ;
@@ -2716,8 +2761,8 @@ impl MarkdownRenderer {
27162761
27172762 fn task_list_marker ( & mut self , checked : bool ) {
27182763 self . ensure_admonition_header ( ) ;
2719- let marker = if checked { "[x] " } else { "[ ] " } ;
2720- self . push_text ( marker , self . current_style ) ;
2764+ self . item_prefix = Some ( ListPrefix :: task ( checked) ) ;
2765+ self . pending_space = false ;
27212766 }
27222767
27232768 fn rule ( & mut self ) {
@@ -2923,9 +2968,10 @@ impl MarkdownRenderer {
29232968 . unwrap_or_else ( || Style :: new ( ) . fg ( Color :: DarkGray ) ) ;
29242969 self . current_line . push ( Span :: styled ( "│ " , border_style) ) ;
29252970 }
2926- if let Some ( prefix) = & self . list_prefix {
2927- self . current_width += display_width ( prefix) ;
2928- self . current_line . push ( Span :: raw ( prefix. clone ( ) ) ) ;
2971+ if let Some ( prefix) = self . item_prefix . as_mut ( ) {
2972+ let prefix = prefix. take_for_line ( ) ;
2973+ self . current_width += display_width ( & prefix) ;
2974+ self . current_line . push ( Span :: raw ( prefix) ) ;
29292975 }
29302976 }
29312977
@@ -2934,8 +2980,8 @@ impl MarkdownRenderer {
29342980 if self . in_block_quote {
29352981 width += 2 ;
29362982 }
2937- if let Some ( prefix) = & self . list_prefix {
2938- width += display_width ( prefix) ;
2983+ if let Some ( prefix) = & self . item_prefix {
2984+ width += prefix. current_width ( ) ;
29392985 }
29402986 width
29412987 }
@@ -3090,6 +3136,19 @@ mod tests {
30903136 . collect ( )
30913137 }
30923138
3139+ fn all_line_text ( rendered : & super :: MarkdownRender ) -> Vec < String > {
3140+ rendered
3141+ . lines
3142+ . iter ( )
3143+ . map ( |line| {
3144+ line. spans
3145+ . iter ( )
3146+ . map ( |span| span. content . as_ref ( ) )
3147+ . collect ( )
3148+ } )
3149+ . collect ( )
3150+ }
3151+
30933152 #[ test]
30943153 fn extracts_link_segments_with_urls ( ) {
30953154 let rendered = render_markdown ( "Go to [ratatui docs](https://github.com/ratatui/)." , 80 , 0 ) ;
@@ -3122,4 +3181,35 @@ mod tests {
31223181 . all( |link| !link. label. starts_with( ' ' ) && !link. label. ends_with( ' ' ) )
31233182 ) ;
31243183 }
3184+
3185+ #[ test]
3186+ fn renders_unchecked_checklist_without_bullet_prefix ( ) {
3187+ let rendered = render_markdown ( "- [ ] todo" , 80 , 0 ) ;
3188+
3189+ assert_eq ! ( all_line_text( & rendered) , vec![ "[ ] todo" ] ) ;
3190+ }
3191+
3192+ #[ test]
3193+ fn renders_checked_checklist_without_bullet_prefix ( ) {
3194+ let rendered = render_markdown ( "- [x] done" , 80 , 0 ) ;
3195+
3196+ assert_eq ! ( all_line_text( & rendered) , vec![ "[x] done" ] ) ;
3197+ }
3198+
3199+ #[ test]
3200+ fn wraps_checklist_items_with_aligned_continuation ( ) {
3201+ let rendered = render_markdown ( "- [ ] hello world" , 10 , 0 ) ;
3202+
3203+ assert_eq ! ( all_line_text( & rendered) , vec![ "[ ] hello" , " world" ] ) ;
3204+ }
3205+
3206+ #[ test]
3207+ fn keeps_bullets_for_non_task_list_items ( ) {
3208+ let rendered = render_markdown ( "- bullet\n - [x] done\n - [ ] todo" , 80 , 0 ) ;
3209+
3210+ assert_eq ! (
3211+ all_line_text( & rendered) ,
3212+ vec![ "• bullet" , "[x] done" , "[ ] todo" ]
3213+ ) ;
3214+ }
31253215}
0 commit comments