11package markdown
22
33import (
4+ _ "embed"
45 "regexp"
56 "strings"
67 "testing"
78
9+ "github.com/charmbracelet/x/ansi"
810 runewidth "github.com/mattn/go-runewidth"
911 "github.com/stretchr/testify/assert"
1012 "github.com/stretchr/testify/require"
@@ -507,52 +509,46 @@ The fast renderer should handle all of these efficiently.
507509
508510func BenchmarkFastRenderer (b * testing.B ) {
509511 r := NewFastRenderer (80 )
510- b .ResetTimer ()
511- for range b .N {
512+ for b .Loop () {
512513 _ , _ = r .Render (benchmarkInput )
513514 }
514515}
515516
516517func BenchmarkGlamourRenderer (b * testing.B ) {
517518 r := NewGlamourRenderer (80 )
518- b .ResetTimer ()
519- for range b .N {
519+ for b .Loop () {
520520 _ , _ = r .Render (benchmarkInput )
521521 }
522522}
523523
524524func BenchmarkFastRendererSmall (b * testing.B ) {
525525 r := NewFastRenderer (80 )
526526 input := "Hello **world**, this is a *test*."
527- b .ResetTimer ()
528- for range b .N {
527+ for b .Loop () {
529528 _ , _ = r .Render (input )
530529 }
531530}
532531
533532func BenchmarkGlamourRendererSmall (b * testing.B ) {
534533 r := NewGlamourRenderer (80 )
535534 input := "Hello **world**, this is a *test*."
536- b .ResetTimer ()
537- for range b .N {
535+ for b .Loop () {
538536 _ , _ = r .Render (input )
539537 }
540538}
541539
542540func BenchmarkFastRendererCodeBlock (b * testing.B ) {
543541 r := NewFastRenderer (80 )
544542 input := "```go\n func main() {\n \t fmt.Println(\" hello\" )\n }\n ```"
545- b .ResetTimer ()
546- for range b .N {
543+ for b .Loop () {
547544 _ , _ = r .Render (input )
548545 }
549546}
550547
551548func BenchmarkGlamourRendererCodeBlock (b * testing.B ) {
552549 r := NewGlamourRenderer (80 )
553- input := "```go\n func main() {\n \t fmt.Println(\" hello\" )\n }\n ```"
554- b .ResetTimer ()
555- for range b .N {
550+ input := "```go\n func main() {\n \t fmt.Println(\" hello`\" )\n }\n ```"
551+ for b .Loop () {
556552 _ , _ = r .Render (input )
557553 }
558554}
@@ -566,48 +562,42 @@ var benchmarkTableInput = `| Name | Age | City | Country | Occupation |
566562
567563func BenchmarkFastRendererTable (b * testing.B ) {
568564 r := NewFastRenderer (80 )
569- b .ResetTimer ()
570- for range b .N {
565+ for b .Loop () {
571566 _ , _ = r .Render (benchmarkTableInput )
572567 }
573568}
574569
575570func BenchmarkGlamourRendererTable (b * testing.B ) {
576571 r := NewGlamourRenderer (80 )
577- b .ResetTimer ()
578- for range b .N {
572+ for b .Loop () {
579573 _ , _ = r .Render (benchmarkTableInput )
580574 }
581575}
582576
583577func BenchmarkFastRendererTableWidth20 (b * testing.B ) {
584578 r := NewFastRenderer (20 )
585- b .ResetTimer ()
586- for range b .N {
579+ for b .Loop () {
587580 _ , _ = r .Render (benchmarkTableInput )
588581 }
589582}
590583
591584func BenchmarkGlamourRendererTableWidth20 (b * testing.B ) {
592585 r := NewGlamourRenderer (20 )
593- b .ResetTimer ()
594- for range b .N {
586+ for b .Loop () {
595587 _ , _ = r .Render (benchmarkTableInput )
596588 }
597589}
598590
599591func BenchmarkFastRendererTableWidth200 (b * testing.B ) {
600592 r := NewFastRenderer (200 )
601- b .ResetTimer ()
602- for range b .N {
593+ for b .Loop () {
603594 _ , _ = r .Render (benchmarkTableInput )
604595 }
605596}
606597
607598func BenchmarkGlamourRendererTableWidth200 (b * testing.B ) {
608599 r := NewGlamourRenderer (200 )
609- b .ResetTimer ()
610- for range b .N {
600+ for b .Loop () {
611601 _ , _ = r .Render (benchmarkTableInput )
612602 }
613603}
@@ -764,3 +754,104 @@ This is a paragraph.
764754 })
765755 }
766756}
757+
758+ func TestInlineCodeRestoresBaseStyle (t * testing.T ) {
759+ t .Parallel ()
760+
761+ // This test verifies that text after inline code has the document's base style restored,
762+ // not just a reset to terminal default.
763+ // Bug: "Hello `there` beautiful" - "beautiful" was appearing with terminal default
764+ // instead of the document's text color.
765+
766+ r := NewFastRenderer (80 )
767+ result , err := r .Render ("Hello `there` beautiful" )
768+ require .NoError (t , err )
769+
770+ seqs := ansiRegex .FindAllString (result , - 1 )
771+
772+ // We should have:
773+ // 1. Code style (foreground + background for inline code)
774+ // 2. Reset
775+ // 3. Base text style restoration (document foreground color)
776+ require .GreaterOrEqual (t , len (seqs ), 3 , "Should have at least 3 ANSI sequences" )
777+
778+ // First sequence should be the code style (has RGB foreground and background)
779+ assert .Contains (t , seqs [0 ], "38;2;" , "Code style should have RGB foreground" )
780+ assert .Contains (t , seqs [0 ], "48;2;" , "Code style should have RGB background" )
781+
782+ // Second sequence should be reset
783+ assert .Equal (t , "\x1b [m" , seqs [1 ], "Second sequence should be reset" )
784+
785+ // Third sequence should be the base text style (document color 252)
786+ assert .Contains (t , seqs [2 ], "38;5;252" , "Third sequence should restore document text color" )
787+ }
788+
789+ func TestInlineCodeTextContent (t * testing.T ) {
790+ t .Parallel ()
791+
792+ r := NewFastRenderer (80 )
793+ result , err := r .Render ("Hello `there` beautiful" )
794+ require .NoError (t , err )
795+
796+ plain := ansi .Strip (result )
797+ require .Contains (t , plain , "Hello there beautiful" )
798+ }
799+
800+ //go:embed testdata/streaming_benchmark.md
801+ var streamingBenchmarkContent string
802+
803+ // splitIntoStreamingChunks splits content into chunks that simulate LLM streaming.
804+ // LLM tokens are typically 3-4 characters, so we use small varying chunk sizes.
805+ func splitIntoStreamingChunks (content string ) []string {
806+ var chunks []string
807+ i := 0
808+ chunkSizes := []int {3 , 4 , 3 , 5 , 2 , 4 , 3 , 4 , 5 , 3 , 2 , 4 , 3 , 4 , 3 , 5 }
809+ sizeIdx := 0
810+
811+ for i < len (content ) {
812+ chunkSize := chunkSizes [sizeIdx % len (chunkSizes )]
813+ end := i + chunkSize
814+ if end > len (content ) {
815+ end = len (content )
816+ }
817+ chunks = append (chunks , content [i :end ])
818+ i = end
819+ sizeIdx ++
820+ }
821+ return chunks
822+ }
823+
824+ // BenchmarkStreamingFastRenderer benchmarks rendering progressively growing markdown.
825+ // This simulates the streaming use case where content arrives in chunks and
826+ // the entire accumulated content is re-rendered on each update.
827+ func BenchmarkStreamingFastRenderer (b * testing.B ) {
828+ chunks := splitIntoStreamingChunks (streamingBenchmarkContent )
829+ r := NewFastRenderer (80 )
830+
831+ b .ResetTimer ()
832+ for b .Loop () {
833+ var accumulated strings.Builder
834+ for _ , chunk := range chunks {
835+ accumulated .WriteString (chunk )
836+ _ , _ = r .Render (accumulated .String ())
837+ }
838+ }
839+ }
840+
841+ // BenchmarkStreamingGlamourRenderer benchmarks glamour with progressively growing markdown.
842+ // Note: glamour's TermRenderer has internal state issues when reused many times,
843+ // so we create a fresh renderer for each benchmark iteration. This adds overhead
844+ // but is necessary to avoid panics in glamour's internal ANSI parser.
845+ func BenchmarkStreamingGlamourRenderer (b * testing.B ) {
846+ chunks := splitIntoStreamingChunks (streamingBenchmarkContent )
847+
848+ b .ResetTimer ()
849+ for b .Loop () {
850+ r := NewGlamourRenderer (80 )
851+ var accumulated strings.Builder
852+ for _ , chunk := range chunks {
853+ accumulated .WriteString (chunk )
854+ _ , _ = r .Render (accumulated .String ())
855+ }
856+ }
857+ }
0 commit comments