@@ -41,6 +41,11 @@ const SGR_ITALIC: &str = "\x1b[3m";
4141/// Heading Start and the restorer; restoring just `\x1b[36m` would silently
4242/// drop the bold half.
4343const SGR_HEADING : & str = "\x1b [1;36m" ;
44+ /// SGR code for the blockquote dim. Strong End (`\x1b[22m`) clears bold
45+ /// *and* faint, and inline Code/Link close with `\x1b[0m` which clears
46+ /// every attribute — so the restorer re-applies this when a tag closes
47+ /// inside a blockquote.
48+ const SGR_BLOCKQUOTE : & str = "\x1b [2m" ;
4449
4550/// True for OpenAI model identifiers (`gpt-*`). Used by the cost
4651/// and token-display paths to route into the OpenAI pricing /
@@ -394,6 +399,7 @@ impl UI {
394399 bold : bool ,
395400 italic : bool ,
396401 in_heading : bool ,
402+ in_blockquote : bool ,
397403 ) -> io:: Result < ( ) > {
398404 if bold {
399405 write ! ( out, "{}" , SGR_BOLD ) ?;
@@ -404,6 +410,9 @@ impl UI {
404410 if in_heading {
405411 write ! ( out, "{}" , SGR_HEADING ) ?;
406412 }
413+ if in_blockquote {
414+ write ! ( out, "{}" , SGR_BLOCKQUOTE ) ?;
415+ }
407416 Ok ( ( ) )
408417 }
409418
@@ -415,6 +424,7 @@ impl UI {
415424 let mut bold = false ;
416425 let mut italic = false ;
417426 let mut in_heading = false ;
427+ let mut in_blockquote = false ;
418428
419429 for event in parser {
420430 match event {
@@ -433,7 +443,7 @@ impl UI {
433443 Event :: End ( TagEnd :: Strong ) => {
434444 bold = false ;
435445 write ! ( out, "\x1b [22m" ) ?;
436- restore_ambient ( out, bold, italic, in_heading) ?;
446+ restore_ambient ( out, bold, italic, in_heading, in_blockquote ) ?;
437447 }
438448 Event :: Start ( Tag :: Emphasis ) => {
439449 italic = true ;
@@ -458,7 +468,7 @@ impl UI {
458468 }
459469 Event :: Code ( code) => {
460470 write ! ( out, "\x1b [38;2;175;215;255m{}\x1b [0m" , code) ?;
461- restore_ambient ( out, bold, italic, in_heading) ?;
471+ restore_ambient ( out, bold, italic, in_heading, in_blockquote ) ?;
462472 }
463473 Event :: Text ( text) => {
464474 if in_code_block {
@@ -489,9 +499,11 @@ impl UI {
489499 writeln ! ( out) ?;
490500 }
491501 Event :: Start ( Tag :: BlockQuote ( _) ) => {
492- write ! ( out, "\x1b [2m> " ) ?;
502+ in_blockquote = true ;
503+ write ! ( out, "{}> " , SGR_BLOCKQUOTE ) ?;
493504 }
494505 Event :: End ( TagEnd :: BlockQuote ( _) ) => {
506+ in_blockquote = false ;
495507 writeln ! ( out, "\x1b [0m" ) ?;
496508 }
497509 Event :: Start ( Tag :: Link { dest_url, .. } ) => {
@@ -504,7 +516,7 @@ impl UI {
504516 }
505517 Event :: End ( TagEnd :: Link ) => {
506518 write ! ( out, "\x1b [0m\x1b ]8;;\x07 " ) ?;
507- restore_ambient ( out, bold, italic, in_heading) ?;
519+ restore_ambient ( out, bold, italic, in_heading, in_blockquote ) ?;
508520 }
509521 Event :: Rule => {
510522 writeln ! ( out, "{}" , "─" . repeat( 40 ) . dimmed( ) ) ?;
@@ -894,6 +906,59 @@ mod markdown_render_tests {
894906 ) ;
895907 }
896908
909+ #[ test]
910+ fn strong_in_blockquote_restores_dim ( ) {
911+ let out = render ( "> **bold** rest" ) ;
912+ let after_strong_end = out
913+ . split ( "\x1b [22m" )
914+ . nth ( 1 )
915+ . expect ( "Strong End must emit \\ x1b[22m" ) ;
916+ let rest_idx = after_strong_end
917+ . find ( " rest" )
918+ . expect ( "trailing text must be present" ) ;
919+ assert ! (
920+ after_strong_end[ ..rest_idx] . contains( SGR_BLOCKQUOTE ) ,
921+ "blockquote dim not restored between Strong End and trailing text; segment={:?}" ,
922+ & after_strong_end[ ..rest_idx]
923+ ) ;
924+ }
925+
926+ #[ test]
927+ fn code_in_blockquote_restores_dim ( ) {
928+ let out = render ( "> `code` rest" ) ;
929+ // The blockquote's own dim opens with \x1b[2m before the inline Code event;
930+ // skip past that prefix so we land between Code's \x1b[0m and the trailing text.
931+ let after_code_reset = out
932+ . split ( "\x1b [0m" )
933+ . nth ( 1 )
934+ . expect ( "inline Code emits \\ x1b[0m" ) ;
935+ let rest_idx = after_code_reset
936+ . find ( " rest" )
937+ . expect ( "trailing text must be present" ) ;
938+ assert ! (
939+ after_code_reset[ ..rest_idx] . contains( SGR_BLOCKQUOTE ) ,
940+ "blockquote dim not restored between inline Code and trailing text; segment={:?}" ,
941+ & after_code_reset[ ..rest_idx]
942+ ) ;
943+ }
944+
945+ #[ test]
946+ fn link_in_blockquote_restores_dim ( ) {
947+ let out = render ( "> [link](https://example.com) rest" ) ;
948+ let after_link_close = out
949+ . split ( "\x1b ]8;;\x07 " )
950+ . nth ( 1 )
951+ . expect ( "Link End must emit OSC 8 close" ) ;
952+ let rest_idx = after_link_close
953+ . find ( " rest" )
954+ . expect ( "trailing text must be present" ) ;
955+ assert ! (
956+ after_link_close[ ..rest_idx] . contains( SGR_BLOCKQUOTE ) ,
957+ "blockquote dim not restored between Link End and trailing text; segment={:?}" ,
958+ & after_link_close[ ..rest_idx]
959+ ) ;
960+ }
961+
897962 #[ test]
898963 fn link_in_emphasis_restores_italic ( ) {
899964 let out = render ( "*italic [link](https://example.com) rest*" ) ;
0 commit comments