tests.rs (29795B)
1 //! Tests for streaming parser behavior. 2 3 use crate::element::Span; 4 use crate::partial::PartialKind; 5 use crate::{InlineElement, InlineStyle, MdElement, StreamParser}; 6 7 /// Helper to resolve a Span against a parser's buffer. 8 fn r<'a>(span: &Span, buf: &'a str) -> &'a str { 9 span.resolve(buf) 10 } 11 12 #[test] 13 fn test_heading_complete() { 14 let mut parser = StreamParser::new(); 15 parser.push("# Hello World\n"); 16 17 assert_eq!(parser.parsed().len(), 1); 18 match &parser.parsed()[0] { 19 MdElement::Heading { level, content } => { 20 assert_eq!(*level, 1); 21 assert_eq!(r(content, parser.buffer()), "Hello World"); 22 } 23 other => panic!("Expected heading, got {:?}", other), 24 } 25 } 26 27 #[test] 28 fn test_heading_streaming() { 29 let mut parser = StreamParser::new(); 30 31 // Stream in chunks 32 parser.push("# Hel"); 33 assert_eq!(parser.parsed().len(), 0); 34 assert!(parser.partial().is_some()); 35 36 parser.push("lo Wor"); 37 assert_eq!(parser.parsed().len(), 0); 38 39 parser.push("ld\n"); 40 assert_eq!(parser.parsed().len(), 1); 41 match &parser.parsed()[0] { 42 MdElement::Heading { level, content } => { 43 assert_eq!(*level, 1); 44 assert_eq!(r(content, parser.buffer()), "Hello World"); 45 } 46 other => panic!("Expected heading, got {:?}", other), 47 } 48 } 49 50 #[test] 51 fn test_code_block_complete() { 52 let mut parser = StreamParser::new(); 53 parser.push("```rust\nfn main() {}\n```\n"); 54 55 assert_eq!(parser.parsed().len(), 1); 56 match &parser.parsed()[0] { 57 MdElement::CodeBlock(cb) => { 58 assert_eq!(cb.language.map(|s| r(&s, parser.buffer())), Some("rust")); 59 assert_eq!(r(&cb.content, parser.buffer()), "fn main() {}\n"); 60 } 61 _ => panic!("Expected code block"), 62 } 63 } 64 65 #[test] 66 fn test_code_block_streaming() { 67 let mut parser = StreamParser::new(); 68 69 parser.push("```py"); 70 // No partial yet — language tag may be incomplete without newline 71 assert!(parser.partial().is_none()); 72 73 parser.push("thon\n"); 74 // Now the full opening fence line is available 75 assert!(parser.in_code_block()); 76 77 parser.push("print('hello')\n"); 78 assert!(parser.in_code_block()); 79 assert_eq!(parser.parsed().len(), 0); 80 81 parser.push("```\n"); 82 assert!(!parser.in_code_block()); 83 assert_eq!(parser.parsed().len(), 1); 84 } 85 86 #[test] 87 fn test_multiple_elements() { 88 let mut parser = StreamParser::new(); 89 parser.push("# Title\n\nSome paragraph text.\n\n## Subtitle\n"); 90 91 assert!(parser.parsed().len() >= 2); 92 } 93 94 #[test] 95 fn test_thematic_break() { 96 let mut parser = StreamParser::new(); 97 parser.push("---\n"); 98 99 assert_eq!(parser.parsed().len(), 1); 100 assert_eq!(parser.parsed()[0], MdElement::ThematicBreak); 101 } 102 103 #[test] 104 fn test_finalize_incomplete_code() { 105 let mut parser = StreamParser::new(); 106 parser.push("```\nunclosed code"); 107 108 assert_eq!(parser.parsed().len(), 0); 109 110 parser.finalize(); 111 112 assert_eq!(parser.parsed().len(), 1); 113 match &parser.parsed()[0] { 114 MdElement::CodeBlock(cb) => { 115 assert!(r(&cb.content, parser.buffer()).contains("unclosed code")); 116 } 117 _ => panic!("Expected code block"), 118 } 119 } 120 121 #[test] 122 fn test_realistic_llm_stream() { 123 let mut parser = StreamParser::new(); 124 125 // Simulate realistic LLM token chunks 126 let chunks = [ 127 "Here's", 128 " a ", 129 "simple", 130 " example:\n\n", 131 "```", 132 "rust", 133 "\n", 134 "fn ", 135 "main() {\n", 136 " println!(\"Hello\");\n", 137 "}", 138 "\n```", 139 "\n\nThat's", 140 " it!", 141 ]; 142 143 for chunk in chunks { 144 parser.push(chunk); 145 } 146 147 parser.finalize(); 148 149 // Should have: paragraph, code block, paragraph 150 assert!( 151 parser.parsed().len() >= 2, 152 "Got {} elements", 153 parser.parsed().len() 154 ); 155 } 156 157 #[test] 158 fn test_heading_levels() { 159 let mut parser = StreamParser::new(); 160 parser.push("# H1\n## H2\n### H3\n"); 161 162 let headings: Vec<_> = parser 163 .parsed() 164 .iter() 165 .filter_map(|e| { 166 if let MdElement::Heading { level, .. } = e { 167 Some(*level) 168 } else { 169 None 170 } 171 }) 172 .collect(); 173 174 assert!(headings.contains(&1)); 175 assert!(headings.contains(&2)); 176 assert!(headings.contains(&3)); 177 } 178 179 #[test] 180 fn test_empty_push() { 181 let mut parser = StreamParser::new(); 182 parser.push(""); 183 parser.push(""); 184 parser.push("# Test\n"); 185 186 assert_eq!(parser.parsed().len(), 1); 187 } 188 189 #[test] 190 fn test_partial_content_visible() { 191 let mut parser = StreamParser::new(); 192 parser.push("```\nsome code"); 193 194 // Should be able to see partial content for speculative rendering 195 let partial = parser.partial_content(); 196 assert!(partial.is_some()); 197 assert!(partial.unwrap().contains("some code")); 198 } 199 200 // Inline formatting tests 201 202 #[test] 203 fn test_inline_bold() { 204 let mut parser = StreamParser::new(); 205 parser.push("This has **bold** text.\n\n"); 206 207 assert_eq!(parser.parsed().len(), 1); 208 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 209 let buf = parser.buffer(); 210 assert!( 211 inlines.iter().any(|e| matches!( 212 e, 213 InlineElement::Styled { style: InlineStyle::Bold, content } if r(content, buf) == "bold" 214 )), 215 "Expected bold element, got: {:?}", 216 inlines 217 ); 218 } else { 219 panic!("Expected paragraph"); 220 } 221 } 222 223 #[test] 224 fn test_inline_italic() { 225 let mut parser = StreamParser::new(); 226 parser.push("This has *italic* text.\n\n"); 227 228 assert_eq!(parser.parsed().len(), 1); 229 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 230 let buf = parser.buffer(); 231 assert!( 232 inlines.iter().any(|e| matches!( 233 e, 234 InlineElement::Styled { style: InlineStyle::Italic, content } if r(content, buf) == "italic" 235 )), 236 "Expected italic element, got: {:?}", 237 inlines 238 ); 239 } else { 240 panic!("Expected paragraph"); 241 } 242 } 243 244 #[test] 245 fn test_inline_code() { 246 let mut parser = StreamParser::new(); 247 parser.push("Use `code` here.\n\n"); 248 249 assert_eq!(parser.parsed().len(), 1); 250 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 251 let buf = parser.buffer(); 252 assert!( 253 inlines.iter().any(|e| matches!( 254 e, 255 InlineElement::Code(s) if r(s, buf) == "code" 256 )), 257 "Expected code element, got: {:?}", 258 inlines 259 ); 260 } else { 261 panic!("Expected paragraph"); 262 } 263 } 264 265 #[test] 266 fn test_inline_link() { 267 let mut parser = StreamParser::new(); 268 parser.push("Check [this link](https://example.com) out.\n\n"); 269 270 assert_eq!(parser.parsed().len(), 1); 271 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 272 let buf = parser.buffer(); 273 assert!(inlines.iter().any(|e| matches!( 274 e, 275 InlineElement::Link { text, url } if r(text, buf) == "this link" && r(url, buf) == "https://example.com" 276 )), "Expected link element, got: {:?}", inlines); 277 } else { 278 panic!("Expected paragraph"); 279 } 280 } 281 282 #[test] 283 fn test_inline_image() { 284 let mut parser = StreamParser::new(); 285 parser.push("See  here.\n\n"); 286 287 assert_eq!(parser.parsed().len(), 1); 288 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 289 let buf = parser.buffer(); 290 assert!( 291 inlines.iter().any(|e| matches!( 292 e, 293 InlineElement::Image { alt, url } if r(alt, buf) == "alt text" && r(url, buf) == "image.png" 294 )), 295 "Expected image element, got: {:?}", 296 inlines 297 ); 298 } else { 299 panic!("Expected paragraph"); 300 } 301 } 302 303 #[test] 304 fn test_inline_strikethrough() { 305 let mut parser = StreamParser::new(); 306 parser.push("This is ~~deleted~~ text.\n\n"); 307 308 assert_eq!(parser.parsed().len(), 1); 309 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 310 let buf = parser.buffer(); 311 assert!(inlines.iter().any(|e| matches!( 312 e, 313 InlineElement::Styled { style: InlineStyle::Strikethrough, content } if r(content, buf) == "deleted" 314 )), "Expected strikethrough element, got: {:?}", inlines); 315 } else { 316 panic!("Expected paragraph"); 317 } 318 } 319 320 #[test] 321 fn test_inline_mixed_formatting() { 322 let mut parser = StreamParser::new(); 323 parser.push("Some **bold**, *italic*, and `code` mixed.\n\n"); 324 325 assert_eq!(parser.parsed().len(), 1); 326 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 327 let has_bold = inlines.iter().any(|e| { 328 matches!( 329 e, 330 InlineElement::Styled { 331 style: InlineStyle::Bold, 332 .. 333 } 334 ) 335 }); 336 let has_italic = inlines.iter().any(|e| { 337 matches!( 338 e, 339 InlineElement::Styled { 340 style: InlineStyle::Italic, 341 .. 342 } 343 ) 344 }); 345 let has_code = inlines.iter().any(|e| matches!(e, InlineElement::Code(_))); 346 347 assert!(has_bold, "Missing bold"); 348 assert!(has_italic, "Missing italic"); 349 assert!(has_code, "Missing code"); 350 } else { 351 panic!("Expected paragraph"); 352 } 353 } 354 355 #[test] 356 fn test_inline_finalize() { 357 let mut parser = StreamParser::new(); 358 parser.push("Text with **bold** formatting"); 359 360 // Not complete yet (no paragraph break) 361 assert_eq!(parser.parsed().len(), 0); 362 363 parser.finalize(); 364 365 // Now should have parsed with inline formatting 366 assert_eq!(parser.parsed().len(), 1); 367 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 368 let buf = parser.buffer(); 369 assert!(inlines.iter().any(|e| matches!( 370 e, 371 InlineElement::Styled { style: InlineStyle::Bold, content } if r(content, buf) == "bold" 372 ))); 373 } else { 374 panic!("Expected paragraph"); 375 } 376 } 377 378 // Paragraph partial kind tests 379 380 #[test] 381 fn test_paragraph_partial_kind() { 382 let mut parser = StreamParser::new(); 383 parser.push("Some text without"); 384 385 // Should have a partial with Paragraph kind, not Heading with level 0 386 let partial = parser.partial().expect("Should have partial"); 387 assert!( 388 matches!(partial.kind, PartialKind::Paragraph), 389 "Expected PartialKind::Paragraph, got {:?}", 390 partial.kind 391 ); 392 } 393 394 #[test] 395 fn test_paragraph_streaming_with_newlines() { 396 let mut parser = StreamParser::new(); 397 398 // Push text with single newline - should continue accumulating 399 parser.push("First line\n"); 400 assert!(parser.partial().is_some()); 401 assert!(matches!( 402 parser.partial().unwrap().kind, 403 PartialKind::Paragraph 404 )); 405 406 parser.push("Second line"); 407 assert_eq!(parser.parsed().len(), 0); // Not complete yet 408 409 // Finalize should emit the accumulated paragraph 410 parser.finalize(); 411 assert_eq!(parser.parsed().len(), 1); 412 assert!(matches!(parser.parsed()[0], MdElement::Paragraph(_))); 413 } 414 415 #[test] 416 fn test_paragraph_double_newline_boundary() { 417 let mut parser = StreamParser::new(); 418 419 // Test when double newline arrives all at once 420 parser.push("Complete paragraph\n\n"); 421 assert_eq!(parser.parsed().len(), 1); 422 assert!(matches!(parser.parsed()[0], MdElement::Paragraph(_))); 423 } 424 425 #[test] 426 fn test_paragraph_finalize_emits_content() { 427 let mut parser = StreamParser::new(); 428 parser.push("Incomplete paragraph without double newline"); 429 430 assert_eq!(parser.parsed().len(), 0); 431 assert!(matches!( 432 parser.partial().unwrap().kind, 433 PartialKind::Paragraph 434 )); 435 436 parser.finalize(); 437 438 assert_eq!(parser.parsed().len(), 1); 439 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 440 let buf = parser.buffer(); 441 assert!(inlines.iter().any(|e| matches!( 442 e, 443 InlineElement::Text(s) if r(s, buf).contains("Incomplete paragraph") 444 ))); 445 } else { 446 panic!("Expected paragraph"); 447 } 448 } 449 450 #[test] 451 fn test_inline_code_with_angle_brackets() { 452 // Test parse_inline directly 453 let input = "Generic Rust: `impl Iterator<Item = &str>` returns a `Result<(), anyhow::Error>`"; 454 let result = crate::parse_inline(input, 0); 455 eprintln!("parse_inline result: {:#?}", result); 456 457 let code_elements: Vec<_> = result 458 .iter() 459 .filter(|e| matches!(e, InlineElement::Code(_))) 460 .collect(); 461 assert_eq!( 462 code_elements.len(), 463 2, 464 "Expected 2 code spans, got: {:#?}", 465 result 466 ); 467 } 468 469 #[test] 470 fn test_streaming_inline_code_with_angle_brackets() { 471 // Test streaming parser with token-by-token delivery 472 let mut parser = StreamParser::new(); 473 let input = 474 "5. Generic Rust: `impl Iterator<Item = &str>` returns a `Result<(), anyhow::Error>`\n\n"; 475 476 // Simulate streaming token by token 477 for ch in input.chars() { 478 parser.push(&ch.to_string()); 479 } 480 481 eprintln!("Parsed elements: {:#?}", parser.parsed()); 482 eprintln!("Partial: {:#?}", parser.partial()); 483 484 // Should have one paragraph with code spans 485 assert!(!parser.parsed().is_empty(), "Should have parsed elements"); 486 487 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 488 let code_elements: Vec<_> = inlines 489 .iter() 490 .filter(|e| matches!(e, InlineElement::Code(_))) 491 .collect(); 492 assert_eq!( 493 code_elements.len(), 494 2, 495 "Expected 2 code spans, got: {:#?}", 496 inlines 497 ); 498 } else { 499 panic!("Expected paragraph, got: {:?}", parser.parsed()[0]); 500 } 501 } 502 503 #[test] 504 fn test_streaming_multiple_code_spans_with_angle_brackets() { 505 // From the screenshot: multiple code spans with nested angle brackets 506 let mut parser = StreamParser::new(); 507 let input = 508 "use `HashMap<K, V>` or `Vec<String>` or `Option<Box<dyn Error>>` in your types\n\n"; 509 510 for ch in input.chars() { 511 parser.push(&ch.to_string()); 512 } 513 514 assert!(!parser.parsed().is_empty(), "Should have parsed elements"); 515 516 if let MdElement::Paragraph(inlines) = &parser.parsed()[0] { 517 let code_elements: Vec<_> = inlines 518 .iter() 519 .filter(|e| matches!(e, InlineElement::Code(_))) 520 .collect(); 521 assert_eq!( 522 code_elements.len(), 523 3, 524 "Expected 3 code spans, got: {:#?}", 525 inlines 526 ); 527 } else { 528 panic!("Expected paragraph, got: {:?}", parser.parsed()[0]); 529 } 530 } 531 532 #[test] 533 fn test_code_block_after_paragraph_single_newline() { 534 // Reproduces: paragraph text ending with ":\n" then "```\n" code block 535 let mut parser = StreamParser::new(); 536 let input = "All events share these common tags:\n```\n[\"d\", \"<session-id>\"]\n```\n"; 537 parser.push(input); 538 539 eprintln!("Before finalize - parsed: {:#?}", parser.parsed()); 540 eprintln!("Before finalize - partial: {:#?}", parser.partial()); 541 542 parser.finalize(); 543 544 eprintln!("After finalize - parsed: {:#?}", parser.parsed()); 545 546 // Should have: paragraph + code block 547 let has_paragraph = parser 548 .parsed() 549 .iter() 550 .any(|e| matches!(e, MdElement::Paragraph(_))); 551 let has_code_block = parser 552 .parsed() 553 .iter() 554 .any(|e| matches!(e, MdElement::CodeBlock(_))); 555 556 assert!(has_paragraph, "Missing paragraph element"); 557 assert!(has_code_block, "Missing code block element"); 558 } 559 560 #[test] 561 fn test_code_block_after_paragraph_single_newline_streaming() { 562 // Same test but streaming char-by-char (how LLM tokens arrive) 563 let mut parser = StreamParser::new(); 564 let input = "All events share these common tags:\n```\n[\"d\", \"<session-id>\"]\n```\n"; 565 566 for ch in input.chars() { 567 parser.push(&ch.to_string()); 568 } 569 570 eprintln!("Before finalize - parsed: {:#?}", parser.parsed()); 571 eprintln!("Before finalize - partial: {:#?}", parser.partial()); 572 eprintln!( 573 "Before finalize - in_code_block: {}", 574 parser.in_code_block() 575 ); 576 577 parser.finalize(); 578 579 eprintln!("After finalize - parsed: {:#?}", parser.parsed()); 580 581 let has_paragraph = parser 582 .parsed() 583 .iter() 584 .any(|e| matches!(e, MdElement::Paragraph(_))); 585 let has_code_block = parser 586 .parsed() 587 .iter() 588 .any(|e| matches!(e, MdElement::CodeBlock(_))); 589 590 assert!(has_paragraph, "Missing paragraph element"); 591 assert!(has_code_block, "Missing code block element"); 592 } 593 594 #[test] 595 fn test_heading_partial_kind_distinct_from_paragraph() { 596 let mut parser = StreamParser::new(); 597 parser.push("# Heading without newline"); 598 599 let partial = parser.partial().expect("Should have partial"); 600 assert!( 601 matches!(partial.kind, PartialKind::Heading { level: 1 }), 602 "Expected PartialKind::Heading {{ level: 1 }}, got {:?}", 603 partial.kind 604 ); 605 } 606 607 // Table tests 608 609 #[test] 610 fn test_table_basic_batch() { 611 let mut parser = StreamParser::new(); 612 parser.push("| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n\n"); 613 614 let tables: Vec<_> = parser 615 .parsed() 616 .iter() 617 .filter(|e| matches!(e, MdElement::Table { .. })) 618 .collect(); 619 assert_eq!( 620 tables.len(), 621 1, 622 "Expected 1 table, got: {:#?}", 623 parser.parsed() 624 ); 625 626 if let MdElement::Table { headers, rows } = &tables[0] { 627 let buf = parser.buffer(); 628 let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect(); 629 assert_eq!(h, &["Name", "Age"]); 630 assert_eq!(rows.len(), 2); 631 let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect(); 632 let r1: Vec<&str> = rows[1].iter().map(|s| r(s, buf)).collect(); 633 assert_eq!(r0, &["Alice", "30"]); 634 assert_eq!(r1, &["Bob", "25"]); 635 } 636 } 637 638 #[test] 639 fn test_table_streaming_char_by_char() { 640 let mut parser = StreamParser::new(); 641 let input = "| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n\n"; 642 643 for ch in input.chars() { 644 parser.push(&ch.to_string()); 645 } 646 647 let tables: Vec<_> = parser 648 .parsed() 649 .iter() 650 .filter(|e| matches!(e, MdElement::Table { .. })) 651 .collect(); 652 assert_eq!( 653 tables.len(), 654 1, 655 "Expected 1 table, got: {:#?}", 656 parser.parsed() 657 ); 658 659 if let MdElement::Table { headers, rows } = &tables[0] { 660 let buf = parser.buffer(); 661 let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect(); 662 assert_eq!(h, &["Name", "Age"]); 663 assert_eq!(rows.len(), 2); 664 let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect(); 665 let r1: Vec<&str> = rows[1].iter().map(|s| r(s, buf)).collect(); 666 assert_eq!(r0, &["Alice", "30"]); 667 assert_eq!(r1, &["Bob", "25"]); 668 } 669 } 670 671 #[test] 672 fn test_table_after_paragraph() { 673 let mut parser = StreamParser::new(); 674 parser.push("Here is a comparison:\n| A | B |\n|---|---|\n| 1 | 2 |\n\n"); 675 676 let has_paragraph = parser 677 .parsed() 678 .iter() 679 .any(|e| matches!(e, MdElement::Paragraph(_))); 680 let has_table = parser 681 .parsed() 682 .iter() 683 .any(|e| matches!(e, MdElement::Table { .. })); 684 685 assert!( 686 has_paragraph, 687 "Missing paragraph, got: {:#?}", 688 parser.parsed() 689 ); 690 assert!(has_table, "Missing table, got: {:#?}", parser.parsed()); 691 } 692 693 #[test] 694 fn test_table_after_paragraph_streaming() { 695 let mut parser = StreamParser::new(); 696 let input = "Here is a comparison:\n| A | B |\n|---|---|\n| 1 | 2 |\n\n"; 697 698 for ch in input.chars() { 699 parser.push(&ch.to_string()); 700 } 701 702 let has_paragraph = parser 703 .parsed() 704 .iter() 705 .any(|e| matches!(e, MdElement::Paragraph(_))); 706 let has_table = parser 707 .parsed() 708 .iter() 709 .any(|e| matches!(e, MdElement::Table { .. })); 710 711 assert!( 712 has_paragraph, 713 "Missing paragraph, got: {:#?}", 714 parser.parsed() 715 ); 716 assert!(has_table, "Missing table, got: {:#?}", parser.parsed()); 717 } 718 719 #[test] 720 fn test_table_then_paragraph() { 721 let mut parser = StreamParser::new(); 722 parser.push("| X | Y |\n|---|---|\n| a | b |\n\nSome text after.\n\n"); 723 724 let has_table = parser 725 .parsed() 726 .iter() 727 .any(|e| matches!(e, MdElement::Table { .. })); 728 let has_paragraph = parser 729 .parsed() 730 .iter() 731 .any(|e| matches!(e, MdElement::Paragraph(_))); 732 733 assert!(has_table, "Missing table, got: {:#?}", parser.parsed()); 734 assert!( 735 has_paragraph, 736 "Missing paragraph, got: {:#?}", 737 parser.parsed() 738 ); 739 } 740 741 #[test] 742 fn test_table_no_separator_not_a_table() { 743 let mut parser = StreamParser::new(); 744 // Two pipe rows but no separator — should not be a table 745 parser.push("| foo | bar |\n| baz | qux |\n\n"); 746 747 let has_table = parser 748 .parsed() 749 .iter() 750 .any(|e| matches!(e, MdElement::Table { .. })); 751 assert!( 752 !has_table, 753 "Should NOT be a table without separator row, got: {:#?}", 754 parser.parsed() 755 ); 756 } 757 758 #[test] 759 fn test_table_uneven_columns() { 760 let mut parser = StreamParser::new(); 761 parser.push("| A | B | C |\n|---|---|---|\n| 1 | 2 |\n| x | y | z |\n\n"); 762 763 let tables: Vec<_> = parser 764 .parsed() 765 .iter() 766 .filter(|e| matches!(e, MdElement::Table { .. })) 767 .collect(); 768 assert_eq!(tables.len(), 1); 769 770 if let MdElement::Table { headers, rows } = &tables[0] { 771 assert_eq!(headers.len(), 3); 772 assert_eq!(rows[0].len(), 2); // Fewer cells than headers 773 assert_eq!(rows[1].len(), 3); 774 } 775 } 776 777 #[test] 778 fn test_table_with_alignment() { 779 // Separator with alignment colons should still be recognized 780 let mut parser = StreamParser::new(); 781 parser.push("| Left | Center | Right |\n|:-----|:------:|------:|\n| a | b | c |\n\n"); 782 783 let tables: Vec<_> = parser 784 .parsed() 785 .iter() 786 .filter(|e| matches!(e, MdElement::Table { .. })) 787 .collect(); 788 assert_eq!( 789 tables.len(), 790 1, 791 "Expected table with alignment separators, got: {:#?}", 792 parser.parsed() 793 ); 794 795 if let MdElement::Table { headers, rows } = &tables[0] { 796 let buf = parser.buffer(); 797 let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect(); 798 assert_eq!(h, &["Left", "Center", "Right"]); 799 assert_eq!(rows.len(), 1); 800 let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect(); 801 assert_eq!(r0, &["a", "b", "c"]); 802 } 803 } 804 805 #[test] 806 fn test_table_finalize_incomplete() { 807 // Table without trailing blank line — finalize should emit it 808 let mut parser = StreamParser::new(); 809 parser.push("| H1 | H2 |\n|---|---|\n| v1 | v2 |"); 810 811 assert_eq!(parser.parsed().len(), 0, "Table shouldn't be complete yet"); 812 813 parser.finalize(); 814 815 let has_table = parser 816 .parsed() 817 .iter() 818 .any(|e| matches!(e, MdElement::Table { .. })); 819 assert!( 820 has_table, 821 "Finalize should emit the table, got: {:#?}", 822 parser.parsed() 823 ); 824 } 825 826 #[test] 827 fn test_table_single_column() { 828 let mut parser = StreamParser::new(); 829 parser.push("| Item |\n|------|\n| Apple |\n| Banana |\n\n"); 830 831 let tables: Vec<_> = parser 832 .parsed() 833 .iter() 834 .filter(|e| matches!(e, MdElement::Table { .. })) 835 .collect(); 836 assert_eq!(tables.len(), 1); 837 838 if let MdElement::Table { headers, rows } = &tables[0] { 839 let buf = parser.buffer(); 840 let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect(); 841 assert_eq!(h, &["Item"]); 842 assert_eq!(rows.len(), 2); 843 } 844 } 845 846 #[test] 847 fn test_table_empty_cells() { 848 let mut parser = StreamParser::new(); 849 parser.push("| A | B |\n|---|---|\n| | val |\n| val | |\n\n"); 850 851 let tables: Vec<_> = parser 852 .parsed() 853 .iter() 854 .filter(|e| matches!(e, MdElement::Table { .. })) 855 .collect(); 856 assert_eq!(tables.len(), 1); 857 858 if let MdElement::Table { headers, rows } = &tables[0] { 859 let buf = parser.buffer(); 860 let h: Vec<&str> = headers.iter().map(|s| r(s, buf)).collect(); 861 assert_eq!(h, &["A", "B"]); 862 let r0: Vec<&str> = rows[0].iter().map(|s| r(s, buf)).collect(); 863 let r1: Vec<&str> = rows[1].iter().map(|s| r(s, buf)).collect(); 864 assert_eq!(r0, &["", "val"]); 865 assert_eq!(r1, &["val", ""]); 866 } 867 } 868 869 #[test] 870 fn test_table_streaming_realistic_llm_chunks() { 871 // Simulate LLM-style token delivery 872 let mut parser = StreamParser::new(); 873 let chunks = [ 874 "Here's", 875 " the comparison:\n", 876 "| Feature", 877 " | ", 878 "Rust | ", 879 "Go |\n", 880 "|---", 881 "---|", 882 "------|------|\n", 883 "| Speed", 884 " | Fast", 885 " | Fast |\n", 886 "| Safety", 887 " | Yes | No |\n", 888 "\nThat's", 889 " the table.", 890 ]; 891 892 for chunk in chunks { 893 parser.push(chunk); 894 } 895 parser.finalize(); 896 897 let has_paragraph = parser 898 .parsed() 899 .iter() 900 .any(|e| matches!(e, MdElement::Paragraph(_))); 901 let has_table = parser 902 .parsed() 903 .iter() 904 .any(|e| matches!(e, MdElement::Table { .. })); 905 906 assert!( 907 has_paragraph, 908 "Missing paragraph, got: {:#?}", 909 parser.parsed() 910 ); 911 assert!(has_table, "Missing table, got: {:#?}", parser.parsed()); 912 913 if let Some(MdElement::Table { headers, rows }) = parser 914 .parsed() 915 .iter() 916 .find(|e| matches!(e, MdElement::Table { .. })) 917 { 918 assert_eq!(headers.len(), 3, "Expected 3 headers, got: {:?}", headers); 919 assert_eq!(rows.len(), 2, "Expected 2 rows, got: {:?}", rows); 920 } 921 } 922 923 #[test] 924 fn test_table_partial_shows_during_streaming() { 925 let mut parser = StreamParser::new(); 926 // Push header + separator, then start a data row 927 parser.push("| A | B |\n|---|---|\n"); 928 929 // Should have a table partial with seen_separator=true 930 let partial = parser.partial().expect("Should have partial"); 931 assert!( 932 matches!( 933 &partial.kind, 934 PartialKind::Table { 935 seen_separator: true, 936 .. 937 } 938 ), 939 "Expected table partial with seen_separator=true, got: {:?}", 940 partial.kind 941 ); 942 } 943 944 #[test] 945 fn test_code_fence_partial_has_language() { 946 // While streaming a code block, the partial should expose the language 947 let mut parser = StreamParser::new(); 948 parser.push("```rust\nfn main() {\n"); 949 950 let partial = parser 951 .partial() 952 .expect("Should have partial while code block is open"); 953 match &partial.kind { 954 PartialKind::CodeFence { language, .. } => { 955 let lang = language.expect("Language should be set during partial"); 956 assert_eq!(lang.resolve(parser.buffer()), "rust"); 957 } 958 other => panic!("Expected CodeFence partial, got: {:?}", other), 959 } 960 // Content should be available too 961 assert_eq!(partial.content(parser.buffer()), "fn main() {\n"); 962 } 963 964 #[test] 965 fn test_code_fence_partial_language_streamed_char_by_char() { 966 // Simulate LLM token-by-token streaming 967 let mut parser = StreamParser::new(); 968 let input = "```python\ndef hello():\n print(\"hi\")\n"; 969 970 for ch in input.chars() { 971 parser.push(&ch.to_string()); 972 } 973 974 // Should still be partial (no closing fence) 975 assert_eq!( 976 parser.parsed().len(), 977 0, 978 "Should not have finalized any elements" 979 ); 980 let partial = parser.partial().expect("Should have partial"); 981 match &partial.kind { 982 PartialKind::CodeFence { language, .. } => { 983 let lang = language.expect("Language should be set"); 984 assert_eq!(lang.resolve(parser.buffer()), "python"); 985 } 986 other => panic!("Expected CodeFence partial, got: {:?}", other), 987 } 988 assert_eq!( 989 partial.content(parser.buffer()), 990 "def hello():\n print(\"hi\")\n" 991 ); 992 } 993 994 #[test] 995 fn test_consecutive_code_blocks_preserve_language() { 996 // Multiple code blocks back-to-back, as an LLM would produce 997 let mut parser = StreamParser::new(); 998 let input = "```rust\nlet x = 1;\n```\n\n```python\nx = 1\n```\n\n```c\nint x = 1;\n```\n"; 999 1000 // Stream in small chunks to simulate LLM output 1001 let chunks: Vec<&str> = input 1002 .as_bytes() 1003 .chunks(5) 1004 .map(|c| std::str::from_utf8(c).unwrap()) 1005 .collect(); 1006 for chunk in &chunks { 1007 parser.push(chunk); 1008 } 1009 1010 let code_blocks: Vec<_> = parser 1011 .parsed() 1012 .iter() 1013 .filter_map(|e| match e { 1014 MdElement::CodeBlock(cb) => Some(cb), 1015 _ => None, 1016 }) 1017 .collect(); 1018 1019 assert!( 1020 code_blocks.len() >= 3, 1021 "Expected 3 code blocks, got {} (parsed: {:?})", 1022 code_blocks.len(), 1023 parser.parsed() 1024 ); 1025 1026 assert_eq!( 1027 code_blocks[0].language.map(|s| r(&s, parser.buffer())), 1028 Some("rust") 1029 ); 1030 assert_eq!( 1031 code_blocks[1].language.map(|s| r(&s, parser.buffer())), 1032 Some("python") 1033 ); 1034 assert_eq!( 1035 code_blocks[2].language.map(|s| r(&s, parser.buffer())), 1036 Some("c") 1037 ); 1038 1039 assert_eq!(r(&code_blocks[0].content, parser.buffer()), "let x = 1;\n"); 1040 assert_eq!(r(&code_blocks[1].content, parser.buffer()), "x = 1\n"); 1041 assert_eq!(r(&code_blocks[2].content, parser.buffer()), "int x = 1;\n"); 1042 }