@@ -98,29 +98,28 @@ fn convert_blocks(blocks: &[Block]) -> Vec<BlockDto> {
9898}
9999
100100/// Convert a single engine block to DTOs, appending to the result vector.
101- /// Some blocks (List, Root) are "unwrapped" and their children are added directly.
101+ /// Root blocks are "unwrapped" and their children are added directly.
102+ /// List blocks are preserved with their ordered flag.
102103fn convert_block_into ( block : & Block , result : & mut Vec < BlockDto > ) {
103- match & block. kind {
104- BlockKind :: Root | BlockKind :: List { .. } => {
105- // Unwrap containers: add children directly to result
106- if let BlockContent :: Children ( children) = & block. content {
107- for child in children {
108- convert_block_into ( child, result) ;
109- }
104+ if block. kind == BlockKind :: Root {
105+ // Unwrap root container: add children directly to result
106+ if let BlockContent :: Children ( children) = & block. content {
107+ for child in children {
108+ convert_block_into ( child, result) ;
110109 }
111- return ;
112110 }
113- _ => { }
111+ return ;
114112 }
115113
116- let ( kind, heading_level, list_marker) = match & block. kind {
117- BlockKind :: Root | BlockKind :: List { .. } => unreachable ! ( ) , // Handled above
118- BlockKind :: Paragraph => ( "paragraph" . to_string ( ) , 0 , None ) ,
119- BlockKind :: Heading { level } => ( "heading" . to_string ( ) , * level, None ) ,
120- BlockKind :: ListItem { marker } => ( "list_item" . to_string ( ) , 0 , Some ( marker. clone ( ) ) ) ,
121- BlockKind :: FencedCode { .. } => ( "code_fence" . to_string ( ) , 0 , None ) ,
122- BlockKind :: ThematicBreak => ( "thematic_break" . to_string ( ) , 0 , None ) ,
123- BlockKind :: BlockQuote => ( "block_quote" . to_string ( ) , 0 , None ) ,
114+ let ( kind, heading_level, list_marker, list_ordered) = match & block. kind {
115+ BlockKind :: Root => unreachable ! ( ) , // Handled above
116+ BlockKind :: Paragraph => ( "paragraph" . to_string ( ) , 0 , None , None ) ,
117+ BlockKind :: Heading { level } => ( "heading" . to_string ( ) , * level, None , None ) ,
118+ BlockKind :: List { ordered } => ( "list" . to_string ( ) , 0 , None , Some ( * ordered) ) ,
119+ BlockKind :: ListItem { marker } => ( "list_item" . to_string ( ) , 0 , Some ( marker. clone ( ) ) , None ) ,
120+ BlockKind :: FencedCode { .. } => ( "code_fence" . to_string ( ) , 0 , None , None ) ,
121+ BlockKind :: ThematicBreak => ( "thematic_break" . to_string ( ) , 0 , None , None ) ,
122+ BlockKind :: BlockQuote => ( "block_quote" . to_string ( ) , 0 , None , None ) ,
124123 } ;
125124
126125 // Convert engine segments to DTOs (engine now provides flat segments)
@@ -142,6 +141,7 @@ fn convert_block_into(block: &Block, result: &mut Vec<BlockDto>) {
142141 kind,
143142 heading_level,
144143 list_marker,
144+ list_ordered,
145145 segments,
146146 children,
147147 } ) ;
@@ -152,12 +152,14 @@ fn convert_block_into(block: &Block, result: &mut Vec<BlockDto>) {
152152pub struct BlockDto {
153153 /// Stable identifier for this block (persists across edits)
154154 pub id : String ,
155- /// Block type (e.g., "heading", "list_item", "paragraph")
155+ /// Block type (e.g., "heading", "list_item", "paragraph", "list" )
156156 pub kind : String ,
157157 /// Heading level (1-6) if this is a heading, 0 otherwise
158158 pub heading_level : u8 ,
159159 /// List marker if this is a list item
160160 pub list_marker : Option < String > ,
161+ /// Whether this is an ordered list (only set for kind="list")
162+ pub list_ordered : Option < bool > ,
161163 /// Parsed inline segments (wiki-links, URLs, plain text)
162164 pub segments : Vec < TextSegmentDto > ,
163165 /// Child blocks (e.g., nested list items)
@@ -417,52 +419,56 @@ mod tests {
417419
418420 #[ test]
419421 fn test_nested_list_tree_structure ( ) {
420- // Verify nested lists produce a proper tree, not a flat list
422+ // Verify nested lists produce a proper tree with list containers preserved
421423 let content = "- parent\n - child 1\n - child 2\n - grandchild" ;
422424 let doc = DocumentHandle :: from_string ( content. to_string ( ) ) . unwrap ( ) ;
423425 let snapshot = doc. get_snapshot ( ) ;
424426
425- // Top level should have the parent list item
427+ // Top level should be a list container
426428 assert_eq ! ( snapshot. blocks. len( ) , 1 ) ;
427- let parent = & snapshot. blocks [ 0 ] ;
429+ let list = & snapshot. blocks [ 0 ] ;
430+ assert_eq ! ( list. kind, "list" ) ;
431+ assert_eq ! ( list. list_ordered, Some ( false ) ) ;
432+
433+ // List should contain the parent list_item
434+ assert_eq ! ( list. children. len( ) , 1 ) ;
435+ let parent = & list. children [ 0 ] ;
428436 assert_eq ! ( parent. kind, "list_item" ) ;
429- // Content is now extracted from segments
430437 assert ! ( segments_to_text( & parent. segments) . contains( "parent" ) ) ;
431438
432- // Parent should have nested items
439+ // Parent should have a nested list container
433440 assert ! (
434441 !parent. children. is_empty( ) ,
435- "Parent should have nested items "
442+ "Parent should have nested content "
436443 ) ;
444+ let nested_list = & parent. children [ 0 ] ;
445+ assert_eq ! ( nested_list. kind, "list" ) ;
437446
438- // Count total nested list items ( child 1, child 2, grandchild)
439- let all_nested = collect_all_blocks ( & parent . children ) ;
440- let nested_list_items : Vec < _ > = all_nested
447+ // Count total list items in tree (parent, child 1, child 2, grandchild)
448+ let all_blocks = collect_all_blocks ( & snapshot . blocks ) ;
449+ let all_list_items : Vec < _ > = all_blocks
441450 . iter ( )
442451 . filter ( |b| b. kind == "list_item" )
443452 . collect ( ) ;
444453 assert_eq ! (
445- nested_list_items . len( ) ,
446- 3 ,
447- "Should have 3 nested items total (child 1, child 2, grandchild)"
454+ all_list_items . len( ) ,
455+ 4 ,
456+ "Should have 4 list items total (parent, child 1, child 2, grandchild)"
448457 ) ;
449458
450- // Verify we can traverse to find specific items
451- // FFI layer "unwraps" List containers, so list items are direct children.
452- // Correct structure: parent.children = [child1, child2] where child2.children = [grandchild]
453- // child1 and child2 are siblings in parent.children, not nested under each other
454- let nested_items: Vec < _ > = parent
459+ // Nested list should have child 1 and child 2 as direct children
460+ let nested_items: Vec < _ > = nested_list
455461 . children
456462 . iter ( )
457463 . filter ( |b| b. kind == "list_item" )
458464 . collect ( ) ;
459465 assert_eq ! (
460466 nested_items. len( ) ,
461467 2 ,
462- "Parent should have 2 direct child items (child 1, child 2)"
468+ "Nested list should have 2 direct child items (child 1, child 2)"
463469 ) ;
464470
465- // child 2 should have the grandchild
471+ // child 2 should have a nested list with the grandchild
466472 let child2 = nested_items
467473 . iter ( )
468474 . find ( |b| segments_to_text ( & b. segments ) . contains ( "child 2" ) ) ;
@@ -485,4 +491,64 @@ mod tests {
485491 // Content is now extracted from segments
486492 assert_eq ! ( segments_to_text( & quote. segments) , "This is a quote" ) ;
487493 }
494+
495+ #[ test]
496+ fn test_list_container_preserved ( ) {
497+ // Verify list containers are preserved with ordered flag
498+ let content = "- item 1\n - item 2" ;
499+ let doc = DocumentHandle :: from_string ( content. to_string ( ) ) . unwrap ( ) ;
500+ let snapshot = doc. get_snapshot ( ) ;
501+
502+ // Top level should be a list, not a list_item
503+ assert_eq ! ( snapshot. blocks. len( ) , 1 ) ;
504+ assert_eq ! ( snapshot. blocks[ 0 ] . kind, "list" ) ;
505+ assert_eq ! ( snapshot. blocks[ 0 ] . list_ordered, Some ( false ) ) ;
506+
507+ // List should contain the list items as children
508+ assert_eq ! ( snapshot. blocks[ 0 ] . children. len( ) , 2 ) ;
509+ let item1 = & snapshot. blocks [ 0 ] . children [ 0 ] ;
510+ let item2 = & snapshot. blocks [ 0 ] . children [ 1 ] ;
511+ assert_eq ! ( item1. kind, "list_item" ) ;
512+ assert_eq ! ( item2. kind, "list_item" ) ;
513+
514+ // Segments should NOT contain the marker (no duplication)
515+ assert_eq ! ( segments_to_text( & item1. segments) , "item 1" ) ;
516+ assert_eq ! ( segments_to_text( & item2. segments) , "item 2" ) ;
517+
518+ // Marker should be separate
519+ assert_eq ! ( item1. list_marker, Some ( "- " . to_string( ) ) ) ;
520+ }
521+
522+ #[ test]
523+ fn test_ordered_list_container ( ) {
524+ // Verify ordered lists have list_ordered = true
525+ let content = "1. first\n 2. second" ;
526+ let doc = DocumentHandle :: from_string ( content. to_string ( ) ) . unwrap ( ) ;
527+ let snapshot = doc. get_snapshot ( ) ;
528+
529+ assert_eq ! ( snapshot. blocks. len( ) , 1 ) ;
530+ assert_eq ! ( snapshot. blocks[ 0 ] . kind, "list" ) ;
531+ assert_eq ! ( snapshot. blocks[ 0 ] . list_ordered, Some ( true ) ) ;
532+ }
533+
534+ #[ test]
535+ fn test_mixed_ordered_unordered_lists ( ) {
536+ // Verify document with both ordered and unordered lists
537+ let content = "# Header\n \n - bullet 1\n - bullet 2\n \n 1. numbered 1\n 2. numbered 2" ;
538+ let doc = DocumentHandle :: from_string ( content. to_string ( ) ) . unwrap ( ) ;
539+ let snapshot = doc. get_snapshot ( ) ;
540+
541+ // Should have: heading, unordered list, ordered list
542+ assert_eq ! ( snapshot. blocks. len( ) , 3 ) ;
543+
544+ assert_eq ! ( snapshot. blocks[ 0 ] . kind, "heading" ) ;
545+
546+ assert_eq ! ( snapshot. blocks[ 1 ] . kind, "list" ) ;
547+ assert_eq ! ( snapshot. blocks[ 1 ] . list_ordered, Some ( false ) ) ;
548+ assert_eq ! ( snapshot. blocks[ 1 ] . children. len( ) , 2 ) ;
549+
550+ assert_eq ! ( snapshot. blocks[ 2 ] . kind, "list" ) ;
551+ assert_eq ! ( snapshot. blocks[ 2 ] . list_ordered, Some ( true ) ) ;
552+ assert_eq ! ( snapshot. blocks[ 2 ] . children. len( ) , 2 ) ;
553+ }
488554}
0 commit comments