Skip to content

Commit fe7fc39

Browse files
committed
restore the blockquote dim after bold, inline code, and links inside a blockquote
1 parent 0ea9c93 commit fe7fc39

1 file changed

Lines changed: 69 additions & 4 deletions

File tree

src/ui/mod.rs

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
4343
const 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

Comments
 (0)